diff --git a/README.md b/README.md index 419123b..486659f 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Twig provides access to two features that may help you extend your patterns, [ma Pattern includes take advantage of the [pattern partial syntax](http://patternlab.io/docs/pattern-including.html) as a shorthand for referencing patterns from across the system without needing to rely on absolute paths. The format: ``` -{% include "[patternType]-[patternName]" }} +{% include "[patternType]-[patternName]" %} ``` For example, let's say we wanted to include the following pattern in a molecule: @@ -76,11 +76,12 @@ Would be used like this in a pattern: ### Template inheritance -The requirements for using template inheritance with Pattern Lab: +How to use [Template Inheritance](http://twig.sensiolabs.org/doc/templates.html#template-inheritance) with Pattern Lab: -* Files must go in `source/_layouts` -* Files must have the extension `.twig` -* The filename will be used as the reference in the `extends` tag +* Files must have the extension `.twig`. +* Files can be extended either by using Pattern Lab's normal shorthand syntax (e.g, `{% extends 'templates-extended-layout'%}`). +* Files can optionally go in `source/_layouts` in order to hide them from the list of patterns and then you can just use the filename as reference (e.g., `{% extends 'extended-layout'%}`). +* Files that are in the same directory can also just use the file name without the shorthand syntax (however, it must include the extension). So if `file1.twig` and `file2.twig` were in same directory, you could place this code in `file2.twig`: `{% extends 'file1.twig' %}`. An example of a simple layout called `base.twig` in `source/_layouts`: @@ -124,6 +125,8 @@ Would be used like this in a pattern: {% endblock %} ``` +All uses of `extends` above also work with `includes`, `embed` and most likely many other Twig Tags. Let us know if you run into interesting or unexpected use cases! + ## Extending Twig Further Twig comes with a number of ways to extend the underlying template parser. You can you can add [extra tags](http://twig.sensiolabs.org/doc/advanced.html#tags), [filters](http://twig.sensiolabs.org/doc/advanced.html#filters), [tests](http://twig.sensiolabs.org/doc/advanced.html#tests), and [functions](http://twig.sensiolabs.org/doc/advanced.html#functions). The Twig PatternEngine tries to simplify these extensions by allowing you to create files in specific folders and then auto-load the extensions for you. Learn more about: @@ -148,7 +151,7 @@ The requirements for using filters with Pattern Lab: * The filter **must** set the variable `$filter` * Only one filter per file (_e.g. can only set `$filter` once per file_) -An example function called `rot13.filter.twig` in `source/_twig-components/filters`: +An example function called `rot13.filter.php` in `source/_twig-components/filters`: ```php =5.3.6", - "pattern-lab/core": "0.*", - "twig/twig": "1.*" + "pattern-lab/core": "^2.0.0", + "twig/twig": "~1.0" }, "extra": { "patternlab": { - "config": { - "lineageMatch": "{%([ ]+)?include ["\\']([A-Za-z0-9-_]+)["\\'](.*)%}", - "lineageMatchKey": 2, + "config": { + "lineageMatch": "{%([ ]+)?(?:include|extends|embed)( |\\()["\\']([\\/.@A-Za-z0-9-_]+)["\\']([\\s\\S+]*?)%}", + "lineageMatchKey": 3, "patternExtension": "twig", "twigDebug": false, + "twigAutoescape": "html", "twigDefaultDateFormat": "", "twigDefaultIntervalFormat": "", "twigMacroExt": "macro.twig", @@ -45,4 +45,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/PatternLab/PatternEngine/Twig/Loaders/FilesystemLoader.php b/src/PatternLab/PatternEngine/Twig/Loaders/FilesystemLoader.php index cbae30a..38f99bd 100644 --- a/src/PatternLab/PatternEngine/Twig/Loaders/FilesystemLoader.php +++ b/src/PatternLab/PatternEngine/Twig/Loaders/FilesystemLoader.php @@ -13,6 +13,7 @@ namespace PatternLab\PatternEngine\Twig\Loaders; use \PatternLab\Config; +use \PatternLab\Dispatcher; use \PatternLab\PatternEngine\Loader; use \PatternLab\PatternEngine\Twig\TwigUtil; @@ -24,37 +25,46 @@ class FilesystemLoader extends Loader { public function __construct($options = array()) { // set-up default vars - $twigDebug = Config::getOption("twigDebug"); + $twigDebug = Config::getOption("twigDebug"); // set-up the paths to be searched for templates - $dirPaths = array(); - $dirPaths[] = $options["templatePath"]; - $dirPaths[] = $options["partialsPath"]; + $filesystemLoaderPaths = array(); + $filesystemLoaderPaths[] = $options["templatePath"]; + $filesystemLoaderPaths[] = $options["partialsPath"]; // see if source/_macros exists. if so add it to be searchable - $macrosPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; + $macrosPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; if (is_dir($macrosPath)) { - $dirPaths[] = $macrosPath; + $filesystemLoaderPaths[] = $macrosPath; } // see if source/_layouts exists. if so add it to be searchable - $layoutsPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_layouts"; + $layoutsPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_layouts"; if (is_dir($layoutsPath)) { - $dirPaths[] = $layoutsPath; + $filesystemLoaderPaths[] = $layoutsPath; } // set-up Twig - $twigLoader = new \Twig_Loader_Filesystem($dirPaths); - $this->instance = new \Twig_Environment($twigLoader, array("debug" => $twigDebug)); + $twigLoader = new \Twig_Loader_Filesystem($filesystemLoaderPaths); + $instance = new \Twig_Environment($twigLoader, array("debug" => $twigDebug)); // customize Twig - $this->instance = TwigUtil::loadFilters($this->instance); - $this->instance = TwigUtil::loadFunctions($this->instance); - $this->instance = TwigUtil::loadTags($this->instance); - $this->instance = TwigUtil::loadTests($this->instance); - $this->instance = TwigUtil::loadDateFormats($this->instance); - $this->instance = TwigUtil::loadDebug($this->instance); - $this->instance = TwigUtil::loadMacros($this->instance); + TwigUtil::setInstance($instance); + TwigUtil::loadFilters(); + TwigUtil::loadFunctions(); + TwigUtil::loadTags(); + TwigUtil::loadTests(); + TwigUtil::loadDateFormats(); + TwigUtil::loadDebug(); + TwigUtil::loadMacros(); + + // set-up the dispatcher + $dispatcherInstance = Dispatcher::getInstance(); + $dispatcherInstance->dispatch("twigLoader.customize"); + $dispatcherInstance->dispatch("twigFilesystemLoader.customize"); + + // get the instance + $this->instance = TwigUtil::getInstance(); } diff --git a/src/PatternLab/PatternEngine/Twig/Loaders/PatternLoader.php b/src/PatternLab/PatternEngine/Twig/Loaders/PatternLoader.php index 5a5cdbd..fa2ce2f 100644 --- a/src/PatternLab/PatternEngine/Twig/Loaders/PatternLoader.php +++ b/src/PatternLab/PatternEngine/Twig/Loaders/PatternLoader.php @@ -14,6 +14,7 @@ namespace PatternLab\PatternEngine\Twig\Loaders; use \PatternLab\Config; +use \PatternLab\Dispatcher; use \PatternLab\PatternEngine\Twig\Loaders\Twig\PatternPartialLoader as Twig_Loader_PatternPartialLoader; use \PatternLab\PatternEngine\Twig\Loaders\Twig\PatternStringLoader as Twig_Loader_PatternStringLoader; use \PatternLab\PatternEngine\Loader; @@ -27,45 +28,83 @@ class PatternLoader extends Loader { public function __construct($options = array()) { // set-up default vars - $twigDebug = Config::getOption("twigDebug"); + $twigDebug = Config::getOption("twigDebug"); + $twigAutoescape = Config::getOption("twigAutoescape"); - // set-up the loader list - $loaders = array(); + // go through various places where things can exist $filesystemLoaderPaths = array(); - $loaders[] = new Twig_Loader_PatternPartialLoader(Config::getOption("patternSourceDir"),array("patternPaths" => $options["patternPaths"])); - // see if source/_macros exists - $macrosPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; + $macrosPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; if (is_dir($macrosPath)) { $filesystemLoaderPaths[] = $macrosPath; } // see if source/_layouts exists. if so add it to be searchable - $layoutsPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_layouts"; + $layoutsPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_layouts"; if (is_dir($layoutsPath)) { $filesystemLoaderPaths[] = $layoutsPath; } + + // add source/_patterns subdirectories for Drupal theme template compatibility + $patternSourceDir = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_patterns"; + $patternObjects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($patternSourceDir), \RecursiveIteratorIterator::SELF_FIRST); + $patternObjects->setFlags(\FilesystemIterator::SKIP_DOTS); + + // sort the returned objects + $patternObjects = iterator_to_array($patternObjects); + ksort($patternObjects); + + foreach ($patternObjects as $name => $object) { + if ($object->isDir()) { + $filesystemLoaderPaths[] = $object->getPathname(); + } + } - // add the paths to the filesystem loader if the paths existed + // set-up the loader list in order that they should be checked + // 1. Patterns 2. Filesystem 3. String + $loaders = array(); + // 1. add Patterns + $loaders[] = new Twig_Loader_PatternPartialLoader(Config::getOption("patternSourceDir"),array("patternPaths" => $options["patternPaths"])); + + // 2. add the paths to the filesystem loader if the paths existed if (count($filesystemLoaderPaths) > 0) { - $loaders[] = new \Twig_Loader_Filesystem($filesystemLoaderPaths); + $filesystemLoader = new \Twig_Loader_Filesystem($filesystemLoaderPaths); + $loaders[] = TwigUtil::addPaths($filesystemLoader, $patternSourceDir); } - - $loaders[] = new \Twig_Loader_String(); + + // Setting loaders and giving plugins a chance to manipulate them + TwigUtil::setLoaders($loaders); + // set-up the dispatcher + $dispatcherInstance = Dispatcher::getInstance(); + $dispatcherInstance->dispatch("twigLoaderPreInit.customize"); + // getting the loaders back + $loaders = TwigUtil::getLoaders(); + + // 3. add String loader + // This *must* go last or no loaders after will work ~ https://github.com/symfony/symfony/issues/10865 + // @todo Remove `Twig_Loader_String` - if a Twig include path is wrong, this outputs the string anyway with no error ~ https://github.com/symfony/symfony/issues/10865 + $loaders[] = new \Twig_Loader_String(); // set-up Twig - $twigLoader = new \Twig_Loader_Chain($loaders); - $this->instance = new \Twig_Environment($twigLoader, array("debug" => $twigDebug)); + $twigLoader = new \Twig_Loader_Chain($loaders); + $instance = new \Twig_Environment($twigLoader, array("debug" => $twigDebug, "autoescape" => $twigAutoescape)); // customize Twig - $this->instance = TwigUtil::loadFilters($this->instance); - $this->instance = TwigUtil::loadFunctions($this->instance); - $this->instance = TwigUtil::loadTags($this->instance); - $this->instance = TwigUtil::loadTests($this->instance); - $this->instance = TwigUtil::loadDateFormats($this->instance); - $this->instance = TwigUtil::loadDebug($this->instance); - $this->instance = TwigUtil::loadMacros($this->instance); + TwigUtil::setInstance($instance); + TwigUtil::loadFilters(); + TwigUtil::loadFunctions(); + TwigUtil::loadTags(); + TwigUtil::loadTests(); + TwigUtil::loadDateFormats(); + TwigUtil::loadDebug(); + TwigUtil::loadMacros(); + + $dispatcherInstance->dispatch("twigLoader.customize"); + $dispatcherInstance->dispatch("twigPatternLoader.customize"); + + // get the instance + $this->instance = TwigUtil::getInstance(); } @@ -77,7 +116,17 @@ public function __construct($options = array()) { */ public function render($options = array()) { - return $this->instance->render($options["pattern"], $options["data"]); + $result = $this->instance->render($options["pattern"], $options["data"]); + // This error handler catches files that didn't render using any of the loaders. + // The most common scenario is when a file's contents get passed to and through `Twig_Loader_String` and + // outputs the raw Twig file contents like `@atoms/buttons/button.twig`. + // @todo Remove this once `Twig_Loader_String` is removed. + if (strpos($result, "@") === 0) { + echo "Twig file not found: " . $result . "\n"; + exit(1); + } else { + return $result; + } } diff --git a/src/PatternLab/PatternEngine/Twig/Loaders/StringLoader.php b/src/PatternLab/PatternEngine/Twig/Loaders/StringLoader.php index b22b491..e81c9b7 100644 --- a/src/PatternLab/PatternEngine/Twig/Loaders/StringLoader.php +++ b/src/PatternLab/PatternEngine/Twig/Loaders/StringLoader.php @@ -13,6 +13,7 @@ namespace PatternLab\PatternEngine\Twig\Loaders; use \PatternLab\Config; +use \PatternLab\Dispatcher; use \PatternLab\PatternEngine\Loader; use \PatternLab\PatternEngine\Twig\TwigUtil; @@ -24,43 +25,52 @@ class StringLoader extends Loader { public function __construct($options = array()) { // set-up the defaults - $twigDebug = Config::getOption("twigDebug"); + $twigDebug = Config::getOption("twigDebug"); - // set-up the loader list - $loaders = array(); + // go through various places where things can exist $filesystemLoaderPaths = array(); // see if source/_macros exists - $macrosPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; + $macrosPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; if (is_dir($macrosPath)) { $filesystemLoaderPaths[] = $macrosPath; } // see if source/_layouts exists. if so add it to be searchable - $layoutsPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_layouts"; + $layoutsPath = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_layouts"; if (is_dir($layoutsPath)) { $filesystemLoaderPaths[] = $layoutsPath; } + // set-up the loader list + $loaders = array(); // add the paths to the filesystem loader if the paths existed if (count($filesystemLoaderPaths) > 0) { - $loaders[] = new \Twig_Loader_Filesystem($filesystemLoaderPaths); + $loaders[] = new \Twig_Loader_Filesystem($filesystemLoaderPaths); } - - $loaders[] = new \Twig_Loader_String(); + $loaders[] = new \Twig_Loader_String(); // set-up Twig - $twigLoader = new \Twig_Loader_Chain($loaders); - $this->instance = new \Twig_Environment($twigLoader, array("debug" => $twigDebug)); + $twigLoader = new \Twig_Loader_Chain($loaders); + $instance = new \Twig_Environment($twigLoader, array("debug" => $twigDebug)); - // customize the loader - $this->instance = TwigUtil::loadFilters($this->instance); - $this->instance = TwigUtil::loadFunctions($this->instance); - $this->instance = TwigUtil::loadTags($this->instance); - $this->instance = TwigUtil::loadTests($this->instance); - $this->instance = TwigUtil::loadDateFormats($this->instance); - $this->instance = TwigUtil::loadDebug($this->instance); - $this->instance = TwigUtil::loadMacros($this->instance); + // customize Twig + TwigUtil::setInstance($instance); + TwigUtil::loadFilters(); + TwigUtil::loadFunctions(); + TwigUtil::loadTags(); + TwigUtil::loadTests(); + TwigUtil::loadDateFormats(); + TwigUtil::loadDebug(); + TwigUtil::loadMacros(); + + // set-up the dispatcher + $dispatcherInstance = Dispatcher::getInstance(); + $dispatcherInstance->dispatch("twigLoader.customize"); + $dispatcherInstance->dispatch("twigStringLoader.customize"); + + // get the instance + $this->instance = TwigUtil::getInstance(); } diff --git a/src/PatternLab/PatternEngine/Twig/TwigUtil.php b/src/PatternLab/PatternEngine/Twig/TwigUtil.php index b761556..64f97da 100644 --- a/src/PatternLab/PatternEngine/Twig/TwigUtil.php +++ b/src/PatternLab/PatternEngine/Twig/TwigUtil.php @@ -1,12 +1,12 @@ ".$dirHR." doesn't exist so filters won't be loaded..."); + public static function getInstance() { + + if (empty(self::$instance)) { + return false; + } + + return self::$instance; + } /** - * Load custom date formats for Twig - * @param {Instance} an instance of the twig engine + * Set an instance of the Twig environment + * @param {Instance} an instance of the Twig environment + */ + public static function setInstance($instance = "") { + + if (empty($instance) || !method_exists($instance,'addGlobal')) { + Console::writeError("please set the instance"); + } + + self::$instance = $instance; + + } + + /** + * Get an instance of the Twig loaders + * + * @return {Array} List of Twig Loaders + */ + public static function getLoaders() { + + if (empty(self::$loaders)) { + return false; + } + + return self::$loaders; + + } + + /** + * Set an instance of the Twig loaders + * @param {Array} List of Twig Loaders + */ + public static function setLoaders($loaders = array()) { + + if (empty($loaders)) { + Console::writeError("please set the loaders"); + } + + self::$loaders = $loaders; + + } + + /** + * Add a loader to the Twig Loaders array + * @param {Loader} A Twig Loader + */ + public static function addLoader($loader) { + + self::$loaders[] = $loader; + + } + + /** + * Registering each directory under `_patterns/` as a namespace. For example, `_patterns/00-atoms/` as `@atoms` + * @param {Instance} an instance of the filesystem Loader + * @param {String} the path to the pattern directory * - * @return {Instance} an instance of the twig engine + * @return {Instance} an instance of the filesystem Loader + */ + public static function addPaths($filesystemLoader, $patternSourceDir) { + + $finder = new Finder(); + $finder->directories()->depth(0)->in($patternSourceDir); + foreach ($finder as $file) { + $pattern = $file->getRelativePathName(); + $patternBits = explode("-",$pattern,2); + $patternTypePath = (((int)$patternBits[0] != 0) || ($patternBits[0] == '00')) ? $patternBits[1] : $pattern; + $filesystemLoader->addPath($file->getPathName(), $patternTypePath); + } + + return $filesystemLoader; + + } + + /** + * Load custom date formats for Twig */ - public static function loadDateFormats($instance) { + public static function loadDateFormats() { $dateFormat = Config::getOption("twigDefaultDateFormat"); $intervalFormat = Config::getOption("twigDefaultIntervalFormat"); if ($dateFormat && $intervalFormat && !empty($dateFormat) && !empty($intervalFormat)) { - $instance->getExtension("core")->setDateFormat($dateFormat, $intervalFormat); + self::$instance->getExtension("core")->setDateFormat($dateFormat, $intervalFormat); } - return $instance; - } /** * Enable the debug options for Twig - * @param {Instance} an instance of the twig engine - * - * @return {Instance} an instance of the twig engine */ - public static function loadDebug($instance) { + public static function loadDebug() { if (Config::getOption("twigDebug")) { - $instance->addExtension(new \Twig_Extension_Debug()); + self::$instance->addExtension(new \Twig_Extension_Debug()); } - return $instance; - } /** * Load filters for the Twig PatternEngine - * @param {Instance} an instance of the twig engine - * - * @return {Instance} an instance of the twig engine */ - public static function loadFilters($instance) { + public static function loadFilters() { // load defaults $filterDir = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_twig-components/filters"; @@ -91,7 +162,7 @@ public static function loadFilters($instance) { // $filter should be defined in the included file if (isset($filter)) { - $instance->addFilter($filter); + self::$instance->addFilter($filter); unset($filter); } @@ -99,23 +170,14 @@ public static function loadFilters($instance) { } - } else { - - self::dirNotExist($filterDir); - } - return $instance; - } /** * Load functions for the Twig PatternEngine - * @param {Instance} an instance of the twig engine - * - * @return {Instance} an instance of the twig engine */ - public static function loadFunctions($instance) { + public static function loadFunctions() { // load defaults $functionDir = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_twig-components/functions"; @@ -138,7 +200,7 @@ public static function loadFunctions($instance) { // $function should be defined in the included file if (isset($function)) { - $instance->addFunction($function); + self::$instance->addFunction($function); unset($function); } @@ -146,23 +208,14 @@ public static function loadFunctions($instance) { } - } else { - - self::dirNotExist($functionDir); - } - return $instance; - } /** * Load macros for the Twig PatternEngine - * @param {Instance} an instance of the twig engine - * - * @return {Instance} an instance of the twig engine */ - public static function loadMacros($instance) { + public static function loadMacros() { // load defaults $macroDir = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_macros"; @@ -182,29 +235,20 @@ public static function loadMacros($instance) { if ($baseName[0] != "_") { // add the macro to the global context - $instance->addGlobal($file->getBasename(".".$macroExt), $instance->loadTemplate($baseName)); + self::$instance->addGlobal($file->getBasename(".".$macroExt), self::$instance->loadTemplate($baseName)); } } - } else { - - self::dirNotExist($macroDir); - } - return $instance; - } /** * Load tags for the Twig PatternEngine - * @param {Instance} an instance of the twig engine - * - * @return {Instance} an instance of the twig engine */ - public static function loadTags($instance) { + public static function loadTags() { // load defaults $tagDir = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_twig-components/tags"; @@ -227,29 +271,20 @@ public static function loadTags($instance) { // Project_{filenameBase}_TokenParser should be defined in the include $className = "Project_".$file->getBasename(".".$tagExt)."_TokenParser"; - $instance->addTokenParser(new $className()); + self::$instance->addTokenParser(new $className()); } } - } else { - - self::dirNotExist($tagDir); - } - return $instance; - } /** * Load functions for the Twig PatternEngine - * @param {Instance} an instance of the twig engine - * - * @return {Instance} an instance of the twig engine */ - public static function loadTests($instance) { + public static function loadTests() { // load defaults $testDir = Config::getOption("sourceDir").DIRECTORY_SEPARATOR."_twig-components/tests"; @@ -272,7 +307,7 @@ public static function loadTests($instance) { // $test should be defined in the included file if (isset($test)) { - $instance->addTest($test); + self::$instance->addTest($test); unset($test); } @@ -280,14 +315,8 @@ public static function loadTests($instance) { } - } else { - - self::dirNotExist($testDir); - } - return $instance; - } }