Skip to content

Commit

Permalink
Add "mix-ins" capability to theme system. (#963)
Browse files Browse the repository at this point in the history
- Includes example, tests and generator tool
  • Loading branch information
demiankatz committed Aug 2, 2017
1 parent 35aebdd commit 97d15fe
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 11 deletions.
11 changes: 11 additions & 0 deletions Gruntfile.js
Expand Up @@ -14,6 +14,17 @@ module.exports = function(grunt) {

// Iterate through theme.config.php files collecting parent themes in search path:
while (config = fs.readFileSync("themes/" + parts[1] + "/theme.config.php", "UTF-8")) {
// First identify mixins:
var mixinMatches = config.match(/["']mixins["']\s*=>\s*\[([^\]]+)\]/);
if (mixinMatches !== null) {
var mixinParts = mixinMatches[1].split(',')
for (var i = 0; i < mixinParts.length; i++) {
parts[1] = mixinParts[i].trim().replace(/['"]/g, '');
retVal.push(parts.join('/') + '/');
}
}

// Now move up to parent theme:
var matches = config.match(/["']extends["']\s*=>\s*['"](\w+)['"]/);

// "extends" set to "false" or missing entirely? We've hit the end of the line:
Expand Down
1 change: 1 addition & 0 deletions module/VuFindConsole/config/module.config.php
Expand Up @@ -43,6 +43,7 @@
'generate/recordroute' => 'generate recordroute [<base>] [<newController>] [<module>]',
'generate/staticroute' => 'generate staticroute [<name>] [<module>]',
'generate/theme' => 'generate theme [<themename>]',
'generate/thememixin' => 'generate thememixin [<name>]',
// harvest/harvest_oai is too complex to represent here; we need to rely on default-route
'harvest/merge-marc' => 'harvest merge-marc [<dir>]',
'import/import-xsl' => 'import import-xsl [--test-only] [--index=] [<xml>] [<properties>]',
Expand Down
Expand Up @@ -320,6 +320,33 @@ public function themeAction()
return $this->getSuccessResponse();
}

/**
* Create a custom theme from the template.
*
* @return \Zend\Console\Response
*/
public function thememixinAction()
{
// Validate command line argument:
$request = $this->getRequest();
$name = $request->getParam('name');
if (empty($name)) {
Console::writeLine("\tNo mixin name found, using \"custom\"");
$name = 'custom';
}

// Use the theme generator to create and configure the theme:
$generator = $this->serviceLocator->get('VuFindTheme\MixinGenerator');
if (!$generator->generate($name)) {
Console::writeLine($generator->getLastError());
return $this->getFailureResponse();
}
Console::writeLine(
"\tFinished. Add to your theme.config.php 'mixins' setting to activate."
);
return $this->getSuccessResponse();
}

/**
* Create a new subclass and factory to override a factory-generated
* service.
Expand Down
14 changes: 14 additions & 0 deletions module/VuFindTheme/Module.php
Expand Up @@ -64,6 +64,8 @@ public function getServiceConfig()
{
return [
'factories' => [
'VuFindTheme\MixinGenerator' =>
'VuFindTheme\Module::getMixinGenerator',
'VuFindTheme\ThemeCompiler' =>
'VuFindTheme\Module::getThemeCompiler',
'VuFindTheme\ThemeGenerator' =>
Expand Down Expand Up @@ -98,6 +100,18 @@ public function getViewHelperConfig()
];
}

/**
* Factory function for MixinGenerator object.
*
* @param ServiceManager $sm Service manager
*
* @return MixinGenerator
*/
public static function getMixinGenerator(ServiceManager $sm)
{
return new MixinGenerator($sm->get('VuFindTheme\ThemeInfo'));
}

/**
* Factory function for ThemeCompiler object.
*
Expand Down
67 changes: 67 additions & 0 deletions module/VuFindTheme/src/VuFindTheme/MixinGenerator.php
@@ -0,0 +1,67 @@
<?php
/**
* Class to generate a new mixin from a template.
*
* PHP version 5
*
* Copyright (C) Villanova University 2017.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* @category VuFind
* @package Theme
* @author Chris Hallberg <challber@villanova.edu>
* @author Demian Katz <demian.katz@villanova.edu>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org Main Site
*/
namespace VuFindTheme;
use Zend\Console\Console;

/**
* Class to generate a new mixin from a template.
*
* @category VuFind
* @package Theme
* @author Chris Hallberg <challber@villanova.edu>
* @author Demian Katz <demian.katz@villanova.edu>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org Main Site
*/
class MixinGenerator extends AbstractThemeUtility
{
/**
* Generate a new mixin from a template.
*
* @param string $name Name of mixin to generate.
* @param string $template Name of template mixin directory
*
* @return bool
*/
public function generate($name, $template = 'local_mixin_example')
{
// Check for existing theme
$baseDir = $this->info->getBaseDir() . '/';
if (realpath($baseDir . $name)) {
return $this->setLastError('Mixin "' . $name . '" already exists');
}
Console::writeLine('Creating new mixin: "' . $name . '"');
$source = $baseDir . $template;
$dest = $baseDir . $name;
Console::writeLine("\tCopying $template");
Console::writeLine("\t\tFrom: " . $source);
Console::writeLine("\t\tTo: " . $dest);
return $this->copyDir($source, $dest);
}
}
3 changes: 3 additions & 0 deletions module/VuFindTheme/src/VuFindTheme/ThemeCompiler.php
Expand Up @@ -124,6 +124,9 @@ protected function mergeConfig($src, $dest)
$dest[$key] = $this
->mergeConfig($value, isset($dest[$key]) ? $dest[$key] : []);
break;
case 'mixins':
// Omit mixin settings entirely
break;
default:
// Default behavior: merge arrays, let existing flat settings
// trump new incoming ones:
Expand Down
58 changes: 47 additions & 11 deletions module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
Expand Up @@ -92,6 +92,18 @@ public function getBaseDir()
return $this->baseDir;
}

/**
* Get the configuration file for the specified mixin.
*
* @param string $mixin Mixin name
*
* @return string
*/
protected function getMixinConfig($mixin)
{
return $this->baseDir . "/$mixin/mixin.config.php";
}

/**
* Get the configuration file for the specified theme.
*
Expand Down Expand Up @@ -136,6 +148,26 @@ public function getTheme()
return $this->currentTheme;
}

/**
* Load configuration for the specified theme (and its mixins, if any) into the
* allThemeInfo property.
*
* @param string $theme Name of theme to load
*
* @return void
*/
protected function loadThemeConfig($theme)
{
// Load theme configuration...
$this->allThemeInfo[$theme] = include $this->getThemeConfig($theme);
// ..and if there are mixins, load those too!
if (isset($this->allThemeInfo[$theme]['mixins'])) {
foreach ($this->allThemeInfo[$theme]['mixins'] as $mix) {
$this->allThemeInfo[$mix] = include $this->getMixinConfig($mix);
}
}
}

/**
* Get all the configuration details related to the current theme.
*
Expand All @@ -149,8 +181,7 @@ public function getThemeInfo()
$this->allThemeInfo = [];
$currentTheme = $this->getTheme();
do {
$this->allThemeInfo[$currentTheme]
= include $this->getThemeConfig($currentTheme);
$this->loadThemeConfig($currentTheme);
$currentTheme = $this->allThemeInfo[$currentTheme]['extends'];
} while ($currentTheme);
}
Expand Down Expand Up @@ -180,16 +211,21 @@ public function findContainingTheme($relativePath, $returnType = false)
$allThemeInfo = $this->getThemeInfo();

while (!empty($currentTheme)) {
foreach ($allPaths as $currentPath) {
$file = "$basePath/$currentTheme/$currentPath";
if (file_exists($file)) {
if (true === $returnType) {
return $file;
} else if (self::RETURN_ALL_DETAILS === $returnType) {
return ['path' => $file, 'theme' => $currentTheme];
$currentThemeSet = array_merge(
(array) $currentTheme,
isset($allThemeInfo[$currentTheme]['mixins'])
? $allThemeInfo[$currentTheme]['mixins'] : []
);
foreach ($currentThemeSet as $theme) {
foreach ($allPaths as $currentPath) {
$path = "$basePath/$theme/$currentPath";
if (file_exists($path)) {
// Depending on return type, send back the requested data:
if (self::RETURN_ALL_DETAILS === $returnType) {
return compact('path', 'theme');
}
return $returnType ? $path : $theme;
}
// Default return type:
return $currentTheme;
}
}
$currentTheme = $allThemeInfo[$currentTheme]['extends'];
Expand Down
@@ -0,0 +1 @@
alert('hello, mixin');
@@ -0,0 +1 @@
// no code in demo file
@@ -0,0 +1,4 @@
<?php
return [
'js' => ['mixin.js'],
];
@@ -0,0 +1,5 @@
<?php
return [
'extends' => 'child',
'mixins' => ['mixin'],
];
Expand Up @@ -136,6 +136,64 @@ public function testStandardCompilation()
$this->assertEquals($expectedConfig, $mergedConfig);
}

/**
* Test the compiler with a mixin.
*
* @return void
*/
public function testStandardCompilationWithMixin()
{
$baseDir = $this->info->getBaseDir();
$parentDir = $baseDir . '/parent';
$childDir = $baseDir . '/child';
$mixinDir = $baseDir . '/mixin';
$compiler = $this->getThemeCompiler();
$result = $compiler->compile('mixin_user', 'compiled');

// Did the compiler report success?
$this->assertEquals('', $compiler->getLastError());
$this->assertTrue($result);

// Was the target directory created with the expected files?
$this->assertTrue(is_dir($this->targetPath));
$this->assertTrue(file_exists("{$this->targetPath}/parent.txt"));
$this->assertTrue(file_exists("{$this->targetPath}/child.txt"));
$this->assertTrue(file_exists("{$this->targetPath}/js/mixin.js"));

// Did the right version of the file that exists in both parent and child
// get copied over?
$this->assertEquals(
file_get_contents("$mixinDir/js/hello.js"),
file_get_contents("{$this->targetPath}/js/hello.js")
);
$this->assertNotEquals(
file_get_contents("$childDir/js/hello.js"),
file_get_contents("{$this->targetPath}/js/hello.js")
);
$this->assertNotEquals(
file_get_contents("$parentDir/js/hello.js"),
file_get_contents("{$this->targetPath}/js/hello.js")
);

// Did the configuration merge correctly?
$expectedConfig = [
'extends' => false,
'css' => ['child.css'],
'js' => ['hello.js', 'extra.js', 'mixin.js'],
'helpers' => [
'factories' => [
'foo' => 'fooOverrideFactory',
'bar' => 'barFactory',
],
'invokables' => [
'xyzzy' => 'Xyzzy',
]
],
];
$mergedConfig = include "{$this->targetPath}/theme.config.php";
$this->assertEquals($expectedConfig, $mergedConfig);
}

/**
* Test overwrite protection.
*
Expand Down
Expand Up @@ -109,6 +109,33 @@ public function testGetThemeInfo()
);
}

/**
* Test theme info with a mixin
*
* @return void
*/
public function testGetThemeInfoWithMixin()
{
$ti = $this->getThemeInfo();
$ti->setTheme('mixin_user');
$expectedChild = include "{$this->fixturePath}/child/theme.config.php";
$expectedParent = include "{$this->fixturePath}/parent/theme.config.php";
$expectedMixin = include "{$this->fixturePath}/mixin/mixin.config.php";
$expectedMixinUser
= include "{$this->fixturePath}/mixin_user/theme.config.php";
$this->assertEquals('parent', $expectedChild['extends']);
$this->assertEquals(false, $expectedParent['extends']);
$this->assertEquals(
[
'mixin' => $expectedMixin,
'mixin_user' => $expectedMixinUser,
'child' => $expectedChild,
'parent' => $expectedParent
],
$ti->getThemeInfo()
);
}

/**
* Test unfindable item.
*
Expand All @@ -135,6 +162,19 @@ public function testFindContainingTheme()
$this->assertEquals($expected, $ti->findContainingTheme('parent.txt', ThemeInfo::RETURN_ALL_DETAILS));
}

/**
* Test findContainingTheme() with a mixin
*
* @return void
*/
public function testFindContainingThemeWithMixin()
{
$ti = $this->getThemeInfo();
$ti->setTheme('mixin_user');
$this->assertEquals('mixin', $ti->findContainingTheme('js/mixin.js'));
$this->assertEquals('child', $ti->findContainingTheme('child.txt'));
}

/**
* Get a test object
*
Expand Down
1 change: 1 addition & 0 deletions themes/local_mixin_example/js/mixin-popup.js
@@ -0,0 +1 @@
alert('Your mixin is loaded; please customize it to remove this annoying example script.');
4 changes: 4 additions & 0 deletions themes/local_mixin_example/mixin.config.php
@@ -0,0 +1,4 @@
<?php
return [
'js' => ['mixin-popup.js'],
];

0 comments on commit 97d15fe

Please sign in to comment.