Skip to content
Snippets Groups Projects
Unverified Commit cc9a120a authored by Derek Reese's avatar Derek Reese Committed by GitHub
Browse files

Merge pull request #1 from cybtachyon/feature/jassmith/RefactorTwigLib

Refactored to use twig from library folder
parents 473020c8 a6bfc9c1
No related branches found
No related tags found
No related merge requests found
......@@ -9,12 +9,52 @@ When pattern configurations are saved, the template is downloaded locally (to mi
Rendered twigs may contain drupal tokens, which are then processed in context.
## Installation
Install the patternkit module as usual, and review the important variables below to determine if you would like to change the defaults.
Install the Twig library into /sites/all/libraries/Twig
```
git clone git://github.com/twigphp/Twig.git -b 1.x /tmp/Twig
mv /tmp/Twig/lib/Twig ${DRUPALDIR}/sites/all/libraries/
rm -rf /tmp/Twig
```
The patternkit module by itself only provides the glue for other modules to present components. Define one by implementing ```hook_patternkit_library```
An example implementation follows
```
/**
* Implements hook_patternkit_library().
*/
function webrh_patternkit_library() {
$libraries = array();
$namespaces = array(
'Web RH Patterns' => 'webrh/src/library',
);
$module_path = drupal_get_path('module', 'webrh');
foreach ($namespaces as $namespace => $path) {
$lib_path = $module_path . DIRECTORY_SEPARATOR . $path;
$libraries[] = new PatternkitDrupalTwigLib($namespace, $lib_path);
}
return $libraries;
}
```
There are two different plugins currently available,
* PatternkitRESTLib
* PatternkitDrupalTwigLib
Use the former for dynamic REST based components, and the latter for locally sourced.
## Important Variables
* patternkit_cache_enabled - Whether or not the metadata and render cache are enabled. (Disable during development)
* patternkit_pl_host - The scheme://hostname:port/ of the PatternLab library host.
* patternkit_default_module_ttl - How long the rendered pattern should be cached.
* patternkit_show_errors - Whether or not to display messages on the site.
* patternkit_log_errors - Whether or not to log errors to php error log.
* ```patternkit_cache_enabled``` - Whether or not the metadata and render cache are enabled. (Disable during development)
* ```patternkit_pl_host``` - The scheme://hostname:port/ of the PatternLab library host.
* ```patternkit_default_module_ttl``` - How long the rendered pattern should be cached.
* ```patternkit_show_errors``` - Whether or not to display messages on the site.
* ```patternkit_log_errors``` - Whether or not to log errors to php error log.
## TODOs
* https://github.com/drupal-pattern-lab/roadmap/issues/8 Solve the problem of mapping Drupal fields to pattern Variables.
......
......@@ -5,7 +5,6 @@
* Utility functions for Patternkit.
*/
/**
* Fetch and cache assets to render this pattern.
*
......
{
"$schema": "http://json-schema.org/draft-04/schema#",
"category": "atom",
"title": "Example",
"type": "object",
"format": "grid",
"properties": {
"text": {
"title": "Text",
"type": "string",
"options": {
"grid_columns": 4
}
}
}
}
Test sample twig template.
{{- string|striptags('')|raw -}}
name="Patternkit"
description="Adds Patternkit patterns to panels as content types."
package="Presentation Framework"
core=7.x
name = "Patternkit"
description = "Adds Patternkit patterns to panels as content types."
package = "Presentation Framework"
core = 7.x
version = 7.x-1.1
dependencies[] = ctools
dependencies[] = panels
files[] = src/PatternkitDrupalCachedLib.php
files[] = src/PatternkitDrupalTwigLib.php
files[] = src/PatternkitEditorConfig.php
files[] = src/PatternkitLibInterface.php
files[] = src/PatternkitTwigWrapper.php
files[] = src/PatternkitPattern.php
files[] = src/PatternkitRESTLib.php
......@@ -69,18 +69,41 @@ function patternkit_flush_metadata_cache() {
* An array of Pattern Kit Library objects.
*/
function patternkit_pattern_libraries() {
// @todo Add caching.
return module_invoke_all('patternkit_library');
static $libraries;
// If undefined, create the libraries metadata.
if (is_null($libraries)) {
if ($cache = cache_get('patternkit_libraries')) {
$libraries = $cache->data;
}
else {
$libraries = module_invoke_all('patternkit_library');
if (!empty($libraries)) {
// Cache for one day.
cache_set('patternkit_library', $libraries, time() + 60 * 60 * 24);
}
}
}
// Triggers initialization of the Twig wrapper.
PatternkitTwigWrapper::getInstance($libraries);
return $libraries;
}
/**
* Implements hook_patternkit_library().
*/
function patternkit_patternkit_library() {
// @todo Replace with a Service that calls a Factory.
$rest_lib = new PatternkitRESTLib();
$libraries = array($rest_lib);
/** @var object $theme */
$libraries = array();
// Load patterns from this module for testing.
$module_path = drupal_get_path('module', 'patternkit');
$lib_path = $module_path . DIRECTORY_SEPARATOR . 'lib';
$libraries[] = new PatternkitDrupalTwigLib('Example', $lib_path);
// Load patterns from themes exposing namespaces.
foreach (list_themes() as $theme_name => $theme) {
if ($theme->engine !== 'twig' || !isset($theme->info['namespaces'])) {
continue;
......
......@@ -65,13 +65,15 @@ abstract class PatternkitDrupalCachedLib implements PatternkitLibInterface {
// If we are requesting data for a specific module type, return just
// that data.
if ($subtype !== NULL && strtolower($subtype) !== 'none') {
$lookup = substr($subtype, 3);
if (!empty($cached_metadata[strtolower($lookup)])) {
return $cached_metadata[strtolower($lookup)];
if (substr($subtype, 0, 3) == 'pk_') {
$subtype = substr($subtype, 3);
}
if (!empty($cached_metadata[strtolower($subtype)])) {
return $cached_metadata[strtolower($subtype)];
}
_patternkit_show_error(
'Patternkit module does not appear to exist (%module), verify module info/usage.',
array('%module' => $lookup)
array('%module' => $subtype)
);
return NULL;
......
......@@ -11,6 +11,8 @@ class PatternkitDrupalTwigLib extends PatternkitDrupalCachedLib {
private $title;
private $metadata;
/**
* PatternkitDrupalTwigLib constructor.
*
......@@ -46,6 +48,8 @@ class PatternkitDrupalTwigLib extends PatternkitDrupalCachedLib {
/**
* Returns the id of the Pattern Library.
*
* The id also functions as the namespace for the twig templates.
*
* @return string
* The Pattern Library id.
*/
......@@ -81,13 +85,20 @@ class PatternkitDrupalTwigLib extends PatternkitDrupalCachedLib {
return t('Unable to lookup the schema for this subtype.');
}
$hostname = $_SERVER['HTTP_HOST'];
$schema_json = drupal_json_encode($pattern->schema);
$starting_json = $config !== NULL ? drupal_json_encode($config->fields)
: $config;
// @todo Move to own JS file & Drupal Settings config var.
$markup = <<<HTML
<div id="editor_holder"></div>
<div id="editor-shadow-injection-target"></div>
<script type="text/javascript">
var target = document.getElementById("editor-shadow-injection-target");
var shadow = target.attachShadow({mode: 'open'});
shadow.innerHTML = '<link rel="stylesheet" id="theme_stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"><link rel="stylesheet" id="icon_stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.css"><div id="editor_holder"></div>';
var data = {};
data.schema = $schema_json;
data.starting = $starting_json;
......@@ -100,14 +111,79 @@ class PatternkitDrupalTwigLib extends PatternkitDrupalCachedLib {
JSONEditor.defaults.options.startval = data.starting;
}
// Override how references are resolved.
JSONEditor.base_url = '//$hostname/'
JSONEditor.prototype._loadExternalRefs = function(schema, callback) {
var self = this;
var refs = this._getExternalRefs(schema);
var done = 0, waiting = 0, callback_fired = false;
$each(refs,function(url) {
if(self.refs[url]) return;
if(!self.options.ajax) throw "Must set ajax option to true to load external ref "+url;
self.refs[url] = 'loading';
waiting++;
var r = new XMLHttpRequest();
var replacement = this.base_url + 'patternkit/ajax/webrh/$1/schema$2'
var uri = url.replace(/(\w+)\.json(#.*)/, replacement);
r.open("GET", uri, true);
r.onreadystatechange = function () {
if (r.readyState != 4) return;
// Request succeeded
if(r.status === 200) {
var response;
try {
response = JSON.parse(r.responseText);
}
catch(e) {
window.console.log(e);
throw "Failed to parse external ref "+url;
}
if(!response || typeof response !== "object") throw "External ref does not contain a valid schema - "+url;
self.refs[url] = response;
self._loadExternalRefs(response,function() {
done++;
if(done >= waiting && !callback_fired) {
callback_fired = true;
callback();
}
});
}
// Request failed
else {
window.console.log(r);
throw "Failed to fetch ref via ajax- "+url;
}
};
r.send();
});
if(!waiting) {
callback();
}
}
// Initialize the editor with a JSON schema
var editor = new JSONEditor(
document.getElementById('editor_holder'), {
target.shadowRoot.getElementById('editor_holder'), {
schema: data.schema,
theme: 'jqueryui',
iconlib: 'jqueryui',
theme: 'bootstrap3',
iconlib: 'fontawesome4',
keep_oneof_values: false,
ajax: true
disable_edit_json: true,
disable_collapse: true,
//disable_properties: true,
//no_additional_properties: true,
ajax: true,
refs: {
"config.json": "/sites/all/modules/custom/webrh/webrh/src/library/atoms/config/api/config.json"
}
}
);
JSONEditor.plugins.sceditor.emoticonsEnabled = false;
......@@ -120,11 +196,15 @@ class PatternkitDrupalTwigLib extends PatternkitDrupalCachedLib {
</script>
HTML;
// @todo Toggle based on developer settings.
drupal_add_js(drupal_get_path('module', 'patternkit')
. '/js/jsoneditor.js');
return $markup;
return array(
'#type' => 'markup',
'#markup' => $markup,
'#attached' => array(
'js' => array(
drupal_get_path('module', 'patternkit') . '/js/jsoneditor.js',
),
),
);
}
/**
......@@ -134,54 +214,92 @@ HTML;
* Array of metadata objects found.
*/
protected function getRawMetadata() {
$id = $this->getId();
$it = new RecursiveDirectoryIterator($this->path);
$filter = array('json');
$metadata = array();
/** @var \SplFileInfo $file */
foreach (new RecursiveIteratorIterator($it) as $file) {
if (!$file->isFile()) {
continue;
}
$file_path = $file->getPath();
$dirs = explode(DIRECTORY_SEPARATOR, $file_path);
// All JSON schema must be in an 'api' folder at this time.
// @todo Add support for custom setups.
// @todo Look at a standard for JSON Schema + JSON Sample data.
$num_dirs = count($dirs);
if ($num_dirs < 2
|| array_pop($dirs) !== 'api') {
continue;
}
$file_ext = $file->getExtension();
if (!in_array(strtolower($file_ext), $filter, TRUE)) {
continue;
// Use static pattern to avoid rebuilding multiple times per request.
if (is_null($this->metadata)) {
$it = new RecursiveDirectoryIterator($this->path);
$filter = ['json', 'twig'];
$this->metadata = [];
$components = [];
/** @var \SplFileInfo $file */
foreach (new RecursiveIteratorIterator($it) as $file) {
// Skip directories and non-files.
if (!$file->isFile()) {
continue;
}
$file_path = $file->getPath();
// Skip tests folders.
if (strpos($file_path, '/tests') !== FALSE) {
continue;
}
// Get the file extension for the file.
$file_ext = $file->getExtension();
if (!in_array(strtolower($file_ext), $filter, TRUE)) {
continue;
}
// We use file_basename as a unique key, it is required that the
// JSON and twig file share this basename.
$file_basename = $file->getBasename('.' . $file_ext);
// Build an array of all the filenames of interest, keyed by name.
$components[$file_basename][$file_ext] = "$file_path/$file_basename.$file_ext";
}
if ($file_contents = file_get_contents($file)) {
$pattern = $this->createPattern(json_decode($file_contents));
$file_basename = $file->getBasename('.json');
$subtype = "pk_$file_basename";
$pattern->subtype = $subtype;
$pattern->url = url("patternkit/ajax/$id/$subtype/schema");
$twig_file = $file_path
. DIRECTORY_SEPARATOR . $file_basename . '.twig';
if (file_exists($twig_file)) {
$pattern->filename = $twig_file;
$pattern->template = file_get_contents($twig_file);
foreach ($components as $module_name => $data) {
// If the component has a json file, create the pattern from it.
if (!empty($data['json']) && $file_contents = file_get_contents($data['json'])) {
$pattern = $this->createPattern(json_decode($file_contents));
$subtype = "pk_$module_name";
$pattern->subtype = $subtype;
// URL is redundant for the twig based components, so we use it to
// store namespace.
$pattern->url = $this->getId();
}
else {
// Create the pattern from defaults.
$pattern = $this->createPattern(
(object) [
'$schema' => 'http =>//json-schema.org/draft-04/schema#',
'category' => 'atom',
'title' => $module_name,
'type' => 'object',
'format' => 'grid',
'properties' => (object) [],
'required' => [],
]
);
}
if (!empty($data['twig'])) {
$twig_file = $data['twig'];
if (file_exists($twig_file)) {
$pattern->filename = $twig_file;
$pattern->template = file_get_contents($twig_file);
}
}
$metadata[$file_basename] = $pattern;
$this->metadata[$module_name] = $pattern;
}
}
foreach ($metadata as $pattern_type => $pattern) {
// Replace any $ref links with relative paths.
if (!isset($pattern->schema->properties)) {
continue;
foreach ($this->metadata as $pattern_type => $pattern) {
// Replace any $ref links with relative paths.
if (!isset($pattern->schema->properties)) {
continue;
}
$pattern->schema->properties = _patternkit_schema_ref(
$pattern->schema->properties,
$this->metadata
);
$this->metadata[$pattern_type] = $pattern;
}
$pattern->schema->properties = _patternkit_schema_ref($pattern->schema->properties,
$metadata);
$metadata[$pattern_type] = $pattern;
}
return $metadata;
return $this->metadata;
}
/**
......@@ -195,14 +313,88 @@ HTML;
* @return string
* The rendered pattern HTML.
*/
public function getRenderedPatternMarkup(PatternkitPattern $pattern,
PatternkitEditorConfig $config) {
public function getRenderedPatternMarkup(
PatternkitPattern $pattern,
PatternkitEditorConfig $config
) {
if (empty($pattern->filename) || empty($config->fields)) {
return '';
}
$template = $pattern->filename;
$variables = $config->fields;
return twig_render_template($template, $variables);
// Add the namespace, if provided.
if (!empty($pattern->url)) {
$template = '@' . $pattern->url . '#/' . $template;
}
return $this->renderTwigTemplate($template, $variables);
}
/**
* Renders a twig template on demand.
*
* @param string $template
* Template filename.
* @param array $variables
* Variables to be assigned to template.
*
* @return string
* Rendered template.
*/
public function renderTwigTemplate($template, array $variables = array()) {
$namespace = '';
$file = $template;
// If a namespace is provided, break it up.
if ($template[0] == '@') {
list($namespace, $file) = explode('#', $template);
}
try {
$bare = basename($file);
try {
$twig = PatternkitTwigWrapper::getInstance()->getTwigInstance();
$template = $twig->loadTemplate("$namespace/$bare");
$content = $template->render($variables);
}
catch (Exception $e) {
$content = t(
'Twig error (!exc} "!error"',
array(
'!exc' => get_class($e),
'!error' => $e->getMessage(),
)
);
watchdog(
'patternkit',
'Twig engine failure: @msg',
array(
'@msg' => $e->getMessage(),
),
WATCHDOG_ERROR
);
}
}
catch (Exception $e) {
$content = t(
'Template (!template) not found',
array(
'!template' => $template,
)
);
watchdog(
'patternkit',
'Twig template not found: @msg',
array(
'@msg' => $template,
),
WATCHDOG_ERROR
);
}
return $content;
}
}
......@@ -10,20 +10,23 @@ class PatternkitPattern {
*
* @param \PatternkitLibInterface $library
* The library the pattern belongs to.
*
* @param object|array $schema
* An optional JSON Schema object to use.
*/
public function __construct(PatternkitLibInterface $library, $schema = array()) {
$this->subtype = NULL;
$this->title = NULL;
$this->html = NULL;
$this->version = NULL;
$this->subtype = NULL;
$this->title = NULL;
$this->html = NULL;
$this->version = NULL;
$this->attachments = NULL;
$this->schema = $schema;
$this->schema = $schema;
// If schema is undefined, initialize empty.
if (empty($schema)) {
return;
}
// Walk the provided schemas and generate the library.
foreach ($schema as $key => $value) {
if ($key !== 'schema' && property_exists($this, (string) $key)) {
$this->{$key} = $value;
......@@ -49,9 +52,9 @@ class PatternkitPattern {
/**
* The subtype for the pattern. Typically "pk_$pattern".
*
* @var string
* Must be unique across the site. Is used by Panels to address config, etc.
*
* This is mostly in use with Drupal to avoid panels namespace conflicts.
* @var string
*/
public $subtype;
......
<?php
/**
* @file
* Patternkit module - Twig wrapper.
*/
/**
* Class PatternkitTwigWrapper.
*/
class PatternkitTwigWrapper {
protected $metadata;
protected $libraries;
protected $twigEngine;
/**
* PatternkitTwigWrapper constructor.
*
* @param array $libraries
* The collection of patterns and metadata.
*
* @return PatternkitTwigWrapper
* Returns the singleton PatternkitTwigWrapper.
*/
public static function getInstance(array $libraries = NULL) {
static $instance;
if (empty($instance)) {
$instance = new static($libraries);
}
return $instance;
}
/**
* PatternkitTwigWrapper constructor.
*
* @param array $libraries
* The collection of patterns and metadata.
*/
protected function __construct(array $libraries) {
if (empty($libraries)) {
watchdog('patternkit', 'Metadata not passed to constructor for patternTwig', WATCHDOG_ERROR);
}
$this->libraries = $libraries;
// Collect all metadata.
$this->metadata = [];
foreach ($libraries as $library) {
$meta = $library->getCachedMetadata();
$this->metadata = array_merge($this->metadata, $meta);
}
// Setup twig environment.
// @TODO: Properly libraryize this.
require_once DRUPAL_ROOT . '/sites/all/libraries/Twig/Autoloader.php';
Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem();
foreach ($this->metadata as $module_name => $module) {
if (!empty($module->filename)) {
$templatesDirectory = DRUPAL_ROOT . DIRECTORY_SEPARATOR . dirname(
$module->filename
);
// We put the namespace into the url, since it's otherwise unused
// and serves a similar purpose.
try {
$loader->addPath($templatesDirectory, $module->url);
// To support empty namespaces (for now)
$loader->addPath($templatesDirectory);
}
catch (Twig_Error_Loader $e) {
drupal_set_message("Error loading $templatesDirectory", 'warning');
watchdog(
'patternkit',
'Error loading @module',
['@module' => $templatesDirectory],
WATCHDOG_WARNING
);
}
}
}
$this->twigEngine = new Twig_Environment(
$loader,
array(
'autorender' => (bool) variable_get('pktwig_auto_render', TRUE),
'autoescape' => (bool) variable_get('pktwig_auto_escape', FALSE),
'auto_reload' => (bool) variable_get('pktwig_auto_reload', FALSE),
'cache' => variable_get('pktwig_template_cache_path', '/tmp/twig_compilation_cache'),
'debug' => (bool) variable_get('pktwig_debug', FALSE),
)
);
}
/**
* Returns a singleton version of the twig template engine.
*
* @return Twig_Environment
* Twig environment object.
*
* @throws \Twig_Error_Loader
* Twig engine instance object.
*/
public function getTwigInstance() {
return $this->twigEngine;
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment