Skip to content
Snippets Groups Projects
Commit 9690e663 authored by Demian Katz's avatar Demian Katz Committed by GitHub
Browse files

Add "mix-ins" capability to theme system. (#963)

- Includes example, tests and generator tool
parent 509f90f9
No related merge requests found
Showing
with 284 additions and 11 deletions
......@@ -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:
......
......@@ -43,6 +43,7 @@ $routes = [
'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>]',
......
......@@ -320,6 +320,33 @@ class GenerateController extends AbstractBase
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.
......
......@@ -64,6 +64,8 @@ class Module
{
return [
'factories' => [
'VuFindTheme\MixinGenerator' =>
'VuFindTheme\Module::getMixinGenerator',
'VuFindTheme\ThemeCompiler' =>
'VuFindTheme\Module::getThemeCompiler',
'VuFindTheme\ThemeGenerator' =>
......@@ -98,6 +100,18 @@ class Module
];
}
/**
* 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.
*
......
<?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);
}
}
......@@ -124,6 +124,9 @@ class ThemeCompiler extends AbstractThemeUtility
$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:
......
......@@ -92,6 +92,18 @@ class ThemeInfo
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.
*
......@@ -136,6 +148,26 @@ class ThemeInfo
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.
*
......@@ -149,8 +181,7 @@ class ThemeInfo
$this->allThemeInfo = [];
$currentTheme = $this->getTheme();
do {
$this->allThemeInfo[$currentTheme]
= include $this->getThemeConfig($currentTheme);
$this->loadThemeConfig($currentTheme);
$currentTheme = $this->allThemeInfo[$currentTheme]['extends'];
} while ($currentTheme);
}
......@@ -180,16 +211,21 @@ class ThemeInfo
$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'];
......
alert('hello, mixin');
\ No newline at end of file
// no code in demo file
\ No newline at end of file
<?php
return [
'js' => ['mixin.js'],
];
<?php
return [
'extends' => 'child',
'mixins' => ['mixin'],
];
......@@ -136,6 +136,64 @@ class ThemeCompilerTest extends Unit\TestCase
$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.
*
......
......@@ -109,6 +109,33 @@ class ThemeInfoTest extends Unit\TestCase
);
}
/**
* 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.
*
......@@ -135,6 +162,19 @@ class ThemeInfoTest extends Unit\TestCase
$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
*
......
alert('Your mixin is loaded; please customize it to remove this annoying example script.');
\ No newline at end of file
<?php
return [
'js' => ['mixin-popup.js'],
];
\ No newline at end of file
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment