Skip to content

Instantly share code, notes, and snippets.

@ErikFontanel
Created July 28, 2017 14:07
Show Gist options
  • Save ErikFontanel/6b25b0388e54463bef59828920a1d884 to your computer and use it in GitHub Desktop.
Save ErikFontanel/6b25b0388e54463bef59828920a1d884 to your computer and use it in GitHub Desktop.
Wordpress Timber Pattern Lab Pattern Loader
/**
* Custom PatternLab Loader for Timber
*
* How to use:
* Place this somewhere in your functions.php, make sure Timber WordPress plugin is installed and activated.
* This code assumes Pattern Lab is installed in the same directory as your theme.
* Change $patternlabSource below if you want to use a different path to Pattern Lab.
*
* This code hooks into Timber when Timber initialises the Twig_Loader_Filesystem
* uses default Timber loader if Pattern Lab patterns aren't found.
*
* Why?
* I wanted to experiment using Pattern Lab templates directly in WordPress production code.
* The Loader Class supports the Pattern Lab naming convention of
* [patternType]/[patternSubtype]/[patternName].[patternExtension]
* with optional numbers for organization/sorting.
*
* Warning: very dirty code ahead
* This hacky code is reversed engineered from the Pattern Lab Twig Pattern Engine Loader Class.
* I don't know much about PHP classes and best practices. Use at your own risk and feel free to improve.
*/
/**
* The Timber Patternlab Partial Loader Class
*/
if(class_exists('Timber')) {
class TimberPatternlabPartialLoader implements \Twig_LoaderInterface {
const MAIN_NAMESPACE = '__main__';
protected $paths = array();
protected $cache = array();
protected $patternPaths = array();
protected $extension = '.twig';
public function __construct($patternPaths) {
$this->patternPaths = $this->getPatterns($patternPaths);
if ($patternPaths) {
$this->setPaths($patternPaths);
}
}
public function getPaths($namespace = self::MAIN_NAMESPACE) {
return isset($this->paths[$namespace]) ? $this->paths[$namespace] : array();
}
public function getNamespaces() {
return array_keys($this->paths);
}
public function setPaths($paths, $namespace = self::MAIN_NAMESPACE) {
if (!is_array($paths)) {
$paths = array($paths);
}
$this->paths[$namespace] = array();
foreach ($paths as $path) {
$this->addPath($path, $namespace);
}
}
public function addPath($path, $namespace = self::MAIN_NAMESPACE) {
// 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, '/\\');
}
public function prependPath($path, $namespace = self::MAIN_NAMESPACE) {
// 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);
}
}
public function getSource($name) {
return file_get_contents($this->findTemplate($name));
}
public function getCacheKey($name) {
return $this->findTemplate($name);
}
public function isFresh($name, $time) {
return filemtime($this->findTemplate($name)) <= $time;
}
public function getPatterns($paths) {
$patternsPath = $paths;
$patternObjects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($patternsPath),RecursiveIteratorIterator::SELF_FIRST);
$patternObjects->setFlags(FilesystemIterator::SKIP_DOTS);
$extension = substr($this->extension, 1);
$patterns = array();
foreach ($patternObjects as $name => $object) {
if ($object->isDir() && $patternObjects->getDepth() == 0) {
$patternBits = explode("-", $object->getBasename(), 2);
$patternType = $patternBits[1];
}
if ($object->isFile() && $object->getExtension() == $extension) {
$patternName = $object->getBasename($this->extension);
$patternPath = str_replace($patternsPath.'/', '', $object->getPathname());
$patterns[$patternType][$patternName] = $patternPath;
}
}
return $patterns;
}
public function findTemplate($name) {
list($partialName,$styleModifier,$parameters) = $this->getPartialInfo($name);
$name = $this->getFileName($partialName, $this->extension);
if (isset($this->cache[$name])) {
return $this->cache[$name];
}
$namespace = self::MAIN_NAMESPACE;
$shortname = $name;
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.'/'.$shortname)) {
return $this->cache[$name] = $path.'/'.$shortname;
}
}
throw new \Twig_Error_Loader(sprintf('Unable to find template "%s" (looked into: %s).', $partialName, implode(', ', $this->paths[$namespace])));
}
function getPatternInfo($name) {
$patternBits = explode("-",$name);
$i = 1;
$k = 2;
$c = count($patternBits);
$patternType = $patternBits[0];
while (!isset($this->patternPaths[$patternType]) && ($i < $c)) {
$patternType .= "-".$patternBits[$i];
$i++;
$k++;
}
$patternBits = explode("-",$name,$k);
$pattern = $patternBits[count($patternBits)-1];
return array($patternType, $pattern);
}
function getPatternFileName($name) {
$patternFileName = "";
list($patternType,$pattern) = $this->getPatternInfo($name);
if (isset($this->patternPaths[$patternType][$pattern])) {
$patternFileName = $this->patternPaths[$patternType][$pattern];
} else if (isset($this->patternPaths[$patternType])) {
foreach($this->patternPaths[$patternType] as $patternMatchKey=>$patternMatchValue) {
$pos = strpos($patternMatchKey,$pattern);
if ($pos !== false) {
$patternFileName = $patternMatchValue;
break;
}
}
}
return $patternFileName;
}
function getFileName($name,$ext) {
$fileName = "";
$dirSep = DIRECTORY_SEPARATOR;
// test to see what kind of path was supplied
$posDash = strpos($name,"-");
$posSlash = strpos($name,$dirSep);
if (($posSlash === false) && ($posDash !== false)) {
$fileName = $this->getPatternFileName($name);
} else {
$fileName = $name;
}
if (substr($fileName, 0 - strlen($ext)) !== $ext) {
$fileName .= $ext;
}
return $fileName;
}
public function getPartialInfo($partial) {
$styleModifier = array();
$parameters = array();
if (strpos($partial, "(") !== false) {
$partialBits = explode("(",$partial,2);
$partial = trim($partialBits[0]);
$parametersString = substr($partialBits[1],0,(strlen($partialBits[1]) - strlen(strrchr($partialBits[1],")"))));
$parameters = $this->parseParameters($parametersString);
}
if (strpos($partial, ":") !== false) {
$partialBits = explode(":",$partial,2);
$partial = $partialBits[0];
$styleModifier = $partialBits[1];
if (strpos($styleModifier, "|") !== false) {
$styleModifierBits = explode("|",$styleModifier);
$styleModifier = join(" ",$styleModifierBits);
}
$styleModifier = array("styleModifier" => $styleModifier);
}
return array($partial,$styleModifier,$parameters);
}
}
}
/**
* Add the loader to the existing Timber loader(s)
*/
add_filter('timber/loader/loader', function ($loader) {
$patternlabSource = get_template_directory().'/patternlab/source';
$filesystemLoaderPaths = array();
$layoutsPath = $patternlabSource . '/_layouts';
if (is_dir($layoutsPath)):
$filesystemLoaderPaths[] = $layoutsPath;
endif;
$patternsPath = $patternlabSource . '/_patterns';
$patternPartialLoader = new \TimberPatternlabPartialLoader($patternsPath);
/**
* The file system loader
*
* iterates all the pattern lab folders looking for twig template files
*
*/
// add source/_patterns
$patternObjects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($patternsPath), RecursiveIteratorIterator::SELF_FIRST); // gotta love PHP
$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();
}
}
$filesystemLoader = new Twig_Loader_Filesystem($filesystemLoaderPaths);
// Add our loader to the end of the chain (i.e. Timber does its lookup, then runs ours)
$chainLoader = new Twig_Loader_Chain([$patternPartialLoader, $filesystemLoader]);
return $chainLoader;
});
@houblon
Copy link

houblon commented Apr 4, 2018

Hi, Erik. This loader is really great. Thanks for sharing it. One thing I'm experiencing is that if I name a pattern file with a number prefix, e.g., "00-header.twig", the number prefix must be present in the file name when I include that template in other pattern files (e.g., {% include '00-header.twig' %}), or I get 'Fatal error: Uncaught Twig_Error_Loader: Template "00-header.twig" is not defined'. So, {% include 'header.twig' %} doesn't work. I've been messing around with it, but I'm not sure how to edit the loader to allow for the prefix in the filename but not require it in includes. Any ideas? Thanks in advance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment