Merge pull request #5830 from open-sausages/pulls/4.0/nested-themes-fixes

API Abstract ThemeManifest into ThemeList
This commit is contained in:
Hamish Friedlander 2016-07-21 15:13:53 +12:00 committed by GitHub
commit 46b15a93ca
25 changed files with 413 additions and 220 deletions

View File

@ -20,7 +20,7 @@ use SilverStripe\Security\Member;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\PermissionProvider; use SilverStripe\Security\PermissionProvider;
use SilverStripe\View\ThemeResourceLoader;
/** /**
@ -408,7 +408,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
&& $candidate->MenuItem->controller && $candidate->MenuItem->controller
&& singleton($candidate->MenuItem->controller)->canView() && singleton($candidate->MenuItem->controller)->canView()
) { ) {
return $this->redirect($candidate->Link); $this->redirect($candidate->Link);
return;
} }
} }
@ -434,11 +435,14 @@ class LeftAndMain extends Controller implements PermissionProvider {
), ),
); );
return Security::permissionFailure($this, $messageSet); Security::permissionFailure($this, $messageSet);
return;
} }
// Don't continue if there's already been a redirection request. // Don't continue if there's already been a redirection request.
if($this->redirectedTo()) return; if($this->redirectedTo()) {
return;
}
// Audit logging hook // Audit logging hook
if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS'); if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
@ -452,29 +456,6 @@ class LeftAndMain extends Controller implements PermissionProvider {
// file because insufficient information exists when that is being processed // file because insufficient information exists when that is being processed
$htmlEditorConfig = HTMLEditorConfig::get_active(); $htmlEditorConfig = HTMLEditorConfig::get_active();
$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang()); $htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
if(!$htmlEditorConfig->getOption('content_css')) {
$cssFiles = array();
$cssFiles[] = FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css';
// Use theme from the site config
if(class_exists('SiteConfig') && ($config = SiteConfig::current_site_config()) && $config->Theme) {
$theme = $config->Theme;
} elseif(Config::inst()->get('SSViewer', 'theme_enabled') && Config::inst()->get('SSViewer', 'theme')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = false;
}
if($theme) $cssFiles[] = THEMES_DIR . "/{$theme}/css/editor.css";
else if(project()) $cssFiles[] = project() . '/css/editor.css';
// Remove files that don't exist
foreach($cssFiles as $k => $cssFile) {
if(!file_exists(BASE_PATH . '/' . $cssFile)) unset($cssFiles[$k]);
}
$htmlEditorConfig->setOption('content_css', implode(',', $cssFiles));
}
Requirements::customScript(" Requirements::customScript("
window.ss = window.ss || {}; window.ss = window.ss || {};
@ -1713,9 +1694,8 @@ class LeftAndMain extends Controller implements PermissionProvider {
if($page) { if($page) {
$navigator = new SilverStripeNavigator($page); $navigator = new SilverStripeNavigator($page);
return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator')); return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
} else {
return false;
} }
return null;
} }
/** /**

View File

@ -77,7 +77,7 @@ require_once 'core/manifest/ConfigManifest.php';
require_once 'core/manifest/ConfigStaticManifest.php'; require_once 'core/manifest/ConfigStaticManifest.php';
require_once 'core/manifest/ClassManifest.php'; require_once 'core/manifest/ClassManifest.php';
require_once 'core/manifest/ManifestFileFinder.php'; require_once 'core/manifest/ManifestFileFinder.php';
require_once 'view/TemplateLoader.php'; require_once 'view/ThemeResourceLoader.php';
require_once 'core/manifest/TokenisedRegularExpression.php'; require_once 'core/manifest/TokenisedRegularExpression.php';
require_once 'control/injector/Injector.php'; require_once 'control/injector/Injector.php';
@ -112,7 +112,7 @@ $configManifest = new SS_ConfigManifest(BASE_PATH, false, $flush);
Config::inst()->pushConfigYamlManifest($configManifest); Config::inst()->pushConfigYamlManifest($configManifest);
// Load template manifest // Load template manifest
SilverStripe\View\TemplateLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest( SilverStripe\View\ThemeResourceLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest(
BASE_PATH, project(), false, $flush BASE_PATH, project(), false, $flush
)); ));

View File

@ -13,7 +13,7 @@ use SilverStripe\Security\Member;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Security\Group; use SilverStripe\Security\Group;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest; use SilverStripe\View\ThemeManifest;
/** /**
@ -848,7 +848,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
SS_ClassLoader::instance()->pushManifest($classManifest, false); SS_ClassLoader::instance()->pushManifest($classManifest, false);
SapphireTest::set_test_class_manifest($classManifest); SapphireTest::set_test_class_manifest($classManifest);
TemplateLoader::instance()->addSet('$default', new ThemeManifest( ThemeResourceLoader::instance()->addSet('$default', new ThemeManifest(
BASE_PATH, project(), true, $flush BASE_PATH, project(), true, $flush
)); ));

View File

@ -51,6 +51,8 @@
a shorthand substitute. a shorthand substitute.
* `FormField->dontEscape` has been removed. Escaping is now managed on a class by class basis. * `FormField->dontEscape` has been removed. Escaping is now managed on a class by class basis.
* `DBString->LimitWordCountXML` removed. Use `LimitWordCount` for XML safe version. * `DBString->LimitWordCountXML` removed. Use `LimitWordCount` for XML safe version.
* `$module` parameter in `themedCSS` and `themedJavascript` removed.
* Theme selector has been removed from SiteConfig. Please use `SSViewer.themes` config instead.
## New API ## New API

View File

@ -1,6 +1,6 @@
<?php <?php
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
/** /**
* Default configuration for HtmlEditor specific to tinymce * Default configuration for HtmlEditor specific to tinymce
@ -417,24 +417,16 @@ class TinyMCEConfig extends HTMLEditorConfig {
*/ */
protected function getEditorCSS() { protected function getEditorCSS() {
$editor = array(); $editor = array();
$editor[] = Controller::join_links(
Director::absoluteBaseURL(),
FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css'
);
foreach(SSViewer::get_themes() as $theme) { // Add standard editor.css
$path = TemplateLoader::instance()->getPath($theme); $editor[] = Director::absoluteURL(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css');
$editorDir = $path . '/css/editor.css';;
if(file_exists(BASE_PATH . '/' . $editorDir)) { // Themed editor.css
$editor[] = Controller::join_links( $themedEditor = ThemeResourceLoader::instance()->findThemedCSS('editor', SSViewer::get_themes());
Director::absoluteBaseURL(), if($themedEditor) {
$editorDir $editor[] = Director::absoluteURL($themedEditor, Director::BASE);
);
break;
}
} }
return $editor; return $editor;
} }

View File

@ -1,6 +1,6 @@
<?php <?php
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest; use SilverStripe\View\ThemeManifest;
/** /**
@ -9,7 +9,7 @@ use SilverStripe\View\ThemeManifest;
* @package framework * @package framework
* @subpackage tests * @subpackage tests
*/ */
class TemplateLoaderTest extends SapphireTest { class ThemeResourceLoaderTest extends SapphireTest {
private $base; private $base;
@ -19,7 +19,7 @@ class TemplateLoaderTest extends SapphireTest {
private $manifest; private $manifest;
/** /**
* @var TemplateLoader * @var ThemeResourceLoader
*/ */
private $loader; private $loader;
@ -28,12 +28,13 @@ class TemplateLoaderTest extends SapphireTest {
*/ */
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
// Fake project root // Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest'; $this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
// New ThemeManifest for that root // New ThemeManifest for that root
$this->manifest = new ThemeManifest($this->base, 'myproject', false, true); $this->manifest = new ThemeManifest($this->base, 'myproject', false, true);
// New Loader for that root // New Loader for that root
$this->loader = new TemplateLoader($this->base); $this->loader = new ThemeResourceLoader($this->base);
$this->loader->addSet('$default', $this->manifest); $this->loader->addSet('$default', $this->manifest);
} }
@ -128,6 +129,58 @@ class TemplateLoaderTest extends SapphireTest {
); );
} }
public function testFindThemedCSS() {
$this->assertEquals(
"myproject/css/project.css",
$this->loader->findThemedCSS('project', ['$default', 'theme'])
);
$this->assertEquals(
"themes/theme/css/project.css",
$this->loader->findThemedCSS('project', ['theme', '$default'])
);
$this->assertEmpty(
$this->loader->findThemedCSS('nofile', ['theme', '$default'])
);
$this->assertEquals(
'module/css/content.css',
$this->loader->findThemedCSS('content', ['/module', 'theme'])
);
$this->assertEquals(
'module/css/content.css',
$this->loader->findThemedCSS('content', ['/module', 'theme', '$default'])
);
$this->assertEquals(
'module/css/content.css',
$this->loader->findThemedCSS('content', ['$default', '/module', 'theme'])
);
}
public function testFindThemedJavascript() {
$this->assertEquals(
"myproject/javascript/project.js",
$this->loader->findThemedJavascript('project', ['$default', 'theme'])
);
$this->assertEquals(
"themes/theme/javascript/project.js",
$this->loader->findThemedJavascript('project', ['theme', '$default'])
);
$this->assertEmpty(
$this->loader->findThemedJavascript('nofile', ['theme', '$default'])
);
$this->assertEquals(
'module/javascript/content.js',
$this->loader->findThemedJavascript('content', ['/module', 'theme'])
);
$this->assertEquals(
'module/javascript/content.js',
$this->loader->findThemedJavascript('content', ['/module', 'theme', '$default'])
);
$this->assertEquals(
'module/javascript/content.js',
$this->loader->findThemedJavascript('content', ['$default', '/module', 'theme'])
);
}
protected function createTestTemplates($templates) { protected function createTestTemplates($templates) {
foreach ($templates as $template) { foreach ($templates as $template) {
file_put_contents($template, ''); file_put_contents($template, '');

View File

@ -0,0 +1,3 @@
.class {
display: block;
}

View File

@ -0,0 +1,3 @@
.class {
display: block;
}

View File

@ -0,0 +1 @@
var i = 0;

View File

@ -0,0 +1 @@
var i = 0;

View File

@ -0,0 +1,3 @@
.class {
display: block;
}

View File

@ -0,0 +1,3 @@
.class {
display: block;
}

View File

@ -0,0 +1,3 @@
.class {
display: block;
}

View File

@ -1,6 +1,6 @@
<?php <?php
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
/** /**
* @package framework * @package framework
@ -17,8 +17,8 @@ class i18nSSLegacyAdapterTest extends SapphireTest {
Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath); Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath);
// Replace old template loader with new one with alternate base path // Replace old template loader with new one with alternate base path
$this->_oldLoader = TemplateLoader::instance(); $this->_oldLoader = ThemeResourceLoader::instance();
TemplateLoader::set_instance(new TemplateLoader($this->alternateBasePath)); ThemeResourceLoader::set_instance(new ThemeResourceLoader($this->alternateBasePath));
$this->_oldTheme = Config::inst()->get('SSViewer', 'theme'); $this->_oldTheme = Config::inst()->get('SSViewer', 'theme');
Config::inst()->update('SSViewer', 'theme', 'testtheme1'); Config::inst()->update('SSViewer', 'theme', 'testtheme1');
@ -42,7 +42,7 @@ class i18nSSLegacyAdapterTest extends SapphireTest {
} }
public function tearDown() { public function tearDown() {
TemplateLoader::set_instance($this->_oldLoader); ThemeResourceLoader::set_instance($this->_oldLoader);
SS_ClassLoader::instance()->popManifest(); SS_ClassLoader::instance()->popManifest();
i18n::set_locale($this->originalLocale); i18n::set_locale($this->originalLocale);
Config::inst()->update('Director', 'alternate_base_folder', null); Config::inst()->update('Director', 'alternate_base_folder', null);

View File

@ -1,7 +1,7 @@
<?php <?php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest; use SilverStripe\View\ThemeManifest;
require_once 'Zend/Translate.php'; require_once 'Zend/Translate.php';
@ -39,8 +39,8 @@ class i18nTest extends SapphireTest {
Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath); Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath);
// Replace old template loader with new one with alternate base path // Replace old template loader with new one with alternate base path
$this->_oldLoader = TemplateLoader::instance(); $this->_oldLoader = ThemeResourceLoader::instance();
TemplateLoader::set_instance($loader = new TemplateLoader($this->alternateBasePath)); ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath));
$loader->addSet('$default', new ThemeManifest( $loader->addSet('$default', new ThemeManifest(
$this->alternateBasePath, project(), false, true $this->alternateBasePath, project(), false, true
)); ));
@ -64,7 +64,7 @@ class i18nTest extends SapphireTest {
} }
public function tearDown() { public function tearDown() {
TemplateLoader::set_instance($this->_oldLoader); ThemeResourceLoader::set_instance($this->_oldLoader);
i18n::set_locale($this->originalLocale); i18n::set_locale($this->originalLocale);
Config::inst()->update('Director', 'alternate_base_folder', null); Config::inst()->update('Director', 'alternate_base_folder', null);
Config::inst()->update('SSViewer', 'theme', $this->_oldTheme); Config::inst()->update('SSViewer', 'theme', $this->_oldTheme);

View File

@ -1,6 +1,6 @@
<?php <?php
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
/** /**
* @package framework * @package framework
@ -38,12 +38,12 @@ class i18nTextCollectorTest extends SapphireTest {
); );
// Replace old template loader with new one with alternate base path // Replace old template loader with new one with alternate base path
$this->_oldLoader = TemplateLoader::instance(); $this->_oldLoader = ThemeResourceLoader::instance();
TemplateLoader::set_instance(new TemplateLoader($this->alternateBasePath)); ThemeResourceLoader::set_instance(new ThemeResourceLoader($this->alternateBasePath));
} }
public function tearDown() { public function tearDown() {
TemplateLoader::set_instance($this->_oldLoader); ThemeResourceLoader::set_instance($this->_oldLoader);
// Pop if added during testing // Pop if added during testing
if(SS_ClassLoader::instance()->getManifest() === $this->manifest) { if(SS_ClassLoader::instance()->getManifest() === $this->manifest) {
SS_ClassLoader::instance()->popManifest(); SS_ClassLoader::instance()->popManifest();

View File

@ -1,7 +1,7 @@
<?php <?php
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler; use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
/** /**
* Requirements tracker for JavaScript and CSS. * Requirements tracker for JavaScript and CSS.
@ -190,13 +190,11 @@ class Requirements implements Flushable {
* the module is used. * the module is used.
* *
* @param string $name The name of the file - eg '/css/File.css' would have the name 'File' * @param string $name The name of the file - eg '/css/File.css' would have the name 'File'
* @param string $module The module to fall back to if the css file does not exist in the
* current theme.
* @param string $media Comma-separated list of media types to use in the link tag * @param string $media Comma-separated list of media types to use in the link tag
* (e.g. 'screen,projector') * (e.g. 'screen,projector')
*/ */
public static function themedCSS($name, $module = null, $media = null) { public static function themedCSS($name, $media = null) {
self::backend()->themedCSS($name, $module, $media); self::backend()->themedCSS($name, $media);
} }
/** /**
@ -207,13 +205,11 @@ class Requirements implements Flushable {
* the module is used. * the module is used.
* *
* @param string $name The name of the file - eg '/javascript/File.js' would have the name 'File' * @param string $name The name of the file - eg '/javascript/File.js' would have the name 'File'
* @param string $module The module to fall back to if the javascript file does not exist in the
* current theme.
* @param string $type Comma-separated list of types to use in the script tag * @param string $type Comma-separated list of types to use in the script tag
* (e.g. 'text/javascript,text/ecmascript') * (e.g. 'text/javascript,text/ecmascript')
*/ */
public static function themedJavascript($name, $module = null, $type = null) { public static function themedJavascript($name, $type = null) {
return self::backend()->themedJavascript($name, $module, $type); return self::backend()->themedJavascript($name, $type);
} }
/** /**
@ -1810,34 +1806,19 @@ class Requirements_Backend
* the module is used. * the module is used.
* *
* @param string $name The name of the file - eg '/css/File.css' would have the name 'File' * @param string $name The name of the file - eg '/css/File.css' would have the name 'File'
* @param string $module The module to fall back to if the css file does not exist in the
* current theme.
* @param string $media Comma-separated list of media types to use in the link tag * @param string $media Comma-separated list of media types to use in the link tag
* (e.g. 'screen,projector') * (e.g. 'screen,projector')
*/ */
public function themedCSS($name, $module = null, $media = null) { public function themedCSS($name, $media = null) {
$css = "/css/$name.css"; $path = ThemeResourceLoader::instance()->findThemedCSS($name, SSViewer::get_themes());
if($path) {
$project = project(); $this->css($path, $media);
$absbase = BASE_PATH . DIRECTORY_SEPARATOR; } else {
$absproject = $absbase . $project; throw new \InvalidArgumentException(
"The css file doesn't exists. Please check if the file $name.css exists in any context or search for "
if(file_exists($absproject . $css)) { . "themedCSS references calling this file in your templates."
return $this->css($project . $css, $media); );
} }
foreach(SSViewer::get_themes() as $theme) {
$path = TemplateLoader::instance()->getPath($theme);
$abspath = BASE_PATH . '/' . $path;
if(file_exists($abspath . $css)) {
return $this->css($path . $css, $media);
}
}
throw new \InvalidArgumentException(
"The css file doesn't exists. Please check if the file $name.css exists in any context or search for "
. "themedCSS references calling this file in your templates."
);
} }
/** /**
@ -1848,38 +1829,23 @@ class Requirements_Backend
* the module is used. * the module is used.
* *
* @param string $name The name of the file - eg '/js/File.js' would have the name 'File' * @param string $name The name of the file - eg '/js/File.js' would have the name 'File'
* @param string $module The module to fall back to if the javascript file does not exist in the
* current theme.
* @param string $type Comma-separated list of types to use in the script tag * @param string $type Comma-separated list of types to use in the script tag
* (e.g. 'text/javascript,text/ecmascript') * (e.g. 'text/javascript,text/ecmascript')
*/ */
public function themedJavascript($name, $module = null, $type = null) { public function themedJavascript($name, $type = null) {
$js = "/javascript/$name.js"; $path = ThemeResourceLoader::instance()->findThemedJavascript($name, SSViewer::get_themes());
if($path) {
$opts = array( $opts = [];
'type' => $type, if($type) {
); $opts['type'] = $type;
}
$project = project(); $this->javascript($path, $opts);
$absbase = BASE_PATH . DIRECTORY_SEPARATOR; } else {
$absproject = $absbase . $project; throw new \InvalidArgumentException(
"The javascript file doesn't exists. Please check if the file $name.js exists in any "
if(file_exists($absproject . $js)) { . "context or search for themedJavascript references calling this file in your templates."
return $this->javascript($project . $js, $opts); );
} }
foreach(SSViewer::get_themes() as $theme) {
$path = TemplateLoader::instance()->getPath($theme);
$abspath = BASE_PATH . '/' . $path;
if(file_exists($abspath . $js)) {
return $this->javascript($path . $js, $opts);
}
}
throw new \InvalidArgumentException(
"The javascript file doesn't exists. Please check if the file $name.js exists in any context or search for "
. "themedJavascript references calling this file in your templates."
);
} }
/** /**

View File

@ -3,7 +3,7 @@
use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\View\TemplateLoader; use SilverStripe\View\ThemeResourceLoader;
/** /**
* This tracks the current scope for an SSViewer instance. It has three goals: * This tracks the current scope for an SSViewer instance. It has three goals:
@ -686,6 +686,11 @@ class SSViewer_DataPresenter extends SSViewer_Scope {
*/ */
class SSViewer implements Flushable { class SSViewer implements Flushable {
/**
* Identifier for the default theme
*/
const DEFAULT_THEME = '$default';
/** /**
* @config * @config
* @var boolean $source_file_comments * @var boolean $source_file_comments
@ -809,6 +814,12 @@ class SSViewer implements Flushable {
return $viewer; return $viewer;
} }
/**
* Assign the list of active themes to apply.
* If default themes should be included add $default as the last entry.
*
* @param array $themes
*/
public static function set_themes($themes = []) { public static function set_themes($themes = []) {
Config::inst()->remove('SSViewer', 'themes'); Config::inst()->remove('SSViewer', 'themes');
Config::inst()->update('SSViewer', 'themes', $themes); Config::inst()->update('SSViewer', 'themes', $themes);
@ -819,14 +830,23 @@ class SSViewer implements Flushable {
} }
public static function get_themes() { public static function get_themes() {
$res = ['$default']; $default = [self::DEFAULT_THEME];
if (Config::inst()->get('SSViewer', 'theme_enabled')) { if (!Config::inst()->get('SSViewer', 'theme_enabled')) {
if ($list = Config::inst()->get('SSViewer', 'themes')) $res = $list; return $default;
elseif ($theme = Config::inst()->get('SSViewer', 'theme')) $res = [$theme, '$default'];
} }
return $res; // Explicit list is assigned
if ($list = Config::inst()->get('SSViewer', 'themes')) {
return $list;
}
// Support legacy behaviour
if ($theme = Config::inst()->get('SSViewer', 'theme')) {
return [$theme, self::DEFAULT_THEME];
}
return $default;
} }
/** /**
@ -835,7 +855,7 @@ class SSViewer implements Flushable {
*/ */
public static function set_theme($theme) { public static function set_theme($theme) {
Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead'); Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
self::set_themes([$theme]); self::set_themes([$theme, self::DEFAULT_THEME]);
} }
/** /**
@ -850,9 +870,9 @@ class SSViewer implements Flushable {
public static function get_templates_by_class($className, $suffix = '', $baseClass = null) { public static function get_templates_by_class($className, $suffix = '', $baseClass = null) {
// Figure out the class name from the supplied context. // Figure out the class name from the supplied context.
if(!is_string($className) || !class_exists($className)) { if(!is_string($className) || !class_exists($className)) {
throw new InvalidArgumentException('SSViewer::get_templates_by_class() expects a valid class name as ' . throw new InvalidArgumentException(
'its first parameter.'); 'SSViewer::get_templates_by_class() expects a valid class name as its first parameter.'
return array(); );
} }
$templates = array(); $templates = array();
$classes = array_reverse(ClassInfo::ancestry($className)); $classes = array_reverse(ClassInfo::ancestry($className));
@ -904,7 +924,7 @@ class SSViewer implements Flushable {
public function setTemplate($templates) { public function setTemplate($templates) {
$this->templates = $templates; $this->templates = $templates;
$this->chosen = TemplateLoader::instance()->findTemplate($templates, self::get_themes()); $this->chosen = ThemeResourceLoader::instance()->findTemplate($templates, self::get_themes());
$this->subTemplates = []; $this->subTemplates = [];
} }
@ -937,7 +957,7 @@ class SSViewer implements Flushable {
* @return boolean * @return boolean
*/ */
public static function hasTemplate($templates) { public static function hasTemplate($templates) {
return (bool)TemplateLoader::instance()->findTemplate($templates, self::get_themes()); return (bool)ThemeResourceLoader::instance()->findTemplate($templates, self::get_themes());
} }
/** /**
@ -1014,7 +1034,7 @@ class SSViewer implements Flushable {
* @return string Full system path to a template file * @return string Full system path to a template file
*/ */
public static function getTemplateFileByType($identifier, $type) { public static function getTemplateFileByType($identifier, $type) {
return TemplateLoader::instance()->findTemplate(['type' => $type, $identifier], self::get_themes()); return ThemeResourceLoader::instance()->findTemplate(['type' => $type, $identifier], self::get_themes());
} }
/** /**

29
view/ThemeList.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace SilverStripe\View;
/**
* Contains references to any number of themes or theme directories
*/
interface ThemeList
{
/**
* Returns a map of all themes information. The map is in the following format:
*
* <code>
* [
* '/mysite',
* 'vendor/module:themename',
* '/framework/admin'
* 'simple'
* ]
* </code>
*
* These may be in any format, including vendor/namespace:path, or /absolute-path,
* but will not include references to any other {@see ThemeContainer} as
* SSViewer::get_themes() does.
*
* @return array
*/
public function getThemes();
}

View File

@ -2,7 +2,8 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use \ManifestFileFinder; use ManifestCache;
use ManifestFileFinder;
/** /**
* A class which builds a manifest of all themes (which is really just a directory called "templates") * A class which builds a manifest of all themes (which is really just a directory called "templates")
@ -10,17 +11,50 @@ use \ManifestFileFinder;
* @package framework * @package framework
* @subpackage manifest * @subpackage manifest
*/ */
class ThemeManifest { class ThemeManifest implements ThemeList {
const TEMPLATES_DIR = 'templates'; const TEMPLATES_DIR = 'templates';
/**
* Base path
*
* @var string
*/
protected $base; protected $base;
/**
* Include tests
*
* @var bool
*/
protected $tests; protected $tests;
/**
* Path to application code
*
* @var string
*/
protected $project; protected $project;
/**
* Cache
*
* @var ManifestCache
*/
protected $cache; protected $cache;
/**
* Cache key
*
* @var string
*/
protected $cacheKey; protected $cacheKey;
/**
* List of theme root directories
*
* @var string
*/
protected $themes = null; protected $themes = null;
/** /**
@ -71,21 +105,10 @@ class ThemeManifest {
)); ));
} }
/**
* Returns a map of all themes information. The map is in the following format:
*
* <code>
* [
* 'mysite',
* 'framework',
* 'framework/admin'
* ]
* </code>
*
* @return array
*/
public function getThemes() { public function getThemes() {
if ($this->themes === null) $this->init(); if ($this->themes === null) {
$this->init();
}
return $this->themes; return $this->themes;
} }
@ -111,26 +134,38 @@ class ThemeManifest {
} }
} }
/**
* Add a directory to the manifest
*
* @param string $basename
* @param string $pathname
* @param int $depth
*/
public function handleDirectory($basename, $pathname, $depth) public function handleDirectory($basename, $pathname, $depth)
{ {
if ($basename == self::TEMPLATES_DIR) { if ($basename !== self::TEMPLATES_DIR) {
// We only want part of the full path, so split into directories return;
$parts = explode('/', $pathname); }
// Take the end (the part relative to base), except the very last directory
$themeParts = array_slice($parts, -$depth, $depth-1);
// Then join again
$path = '/'.implode('/', $themeParts);
// If this is in the project, add to beginning of list. Else add to end. // We only want part of the full path, so split into directories
if ($themeParts[0] == $this->project) { $parts = explode('/', $pathname);
array_unshift($this->themes, $path); // Take the end (the part relative to base), except the very last directory
} $themeParts = array_slice($parts, -$depth, $depth-1);
else { // Then join again
array_push($this->themes, $path); $path = '/'.implode('/', $themeParts);
}
// If this is in the project, add to beginning of list. Else add to end.
if ($themeParts[0] == $this->project) {
array_unshift($this->themes, $path);
}
else {
array_push($this->themes, $path);
} }
} }
/**
* Initialise the manifest
*/
protected function init() { protected function init() {
if ($data = $this->cache->load($this->cacheKey)) { if ($data = $this->cache->load($this->cacheKey)) {
$this->themes = $data; $this->themes = $data;

View File

@ -2,31 +2,44 @@
namespace SilverStripe\View; namespace SilverStripe\View;
use Deprecation;
/** /**
* Handles finding templates from a stack of template manifest objects. * Handles finding templates from a stack of template manifest objects.
* *
* @package framework * @package framework
* @subpackage view * @subpackage view
*/ */
class TemplateLoader { class ThemeResourceLoader {
/** /**
* @var TemplateLoader * @var ThemeResourceLoader
*/ */
private static $instance; private static $instance;
protected $base; protected $base;
/** /**
* @var ThemeManifest[] * List of template "sets" that contain a test manifest, and have an alias.
* E.g. '$default'
*
* @var ThemeList[]
*/ */
protected $sets = []; protected $sets = [];
/**
* @return ThemeResourceLoader
*/
public static function instance() { public static function instance() {
return self::$instance ? self::$instance : self::$instance = new self(); return self::$instance ? self::$instance : self::$instance = new self();
} }
public static function set_instance(TemplateLoader $instance) { /**
* Set instance
*
* @param ThemeResourceLoader $instance
*/
public static function set_instance(ThemeResourceLoader $instance) {
self::$instance = $instance; self::$instance = $instance;
} }
@ -38,12 +51,25 @@ class TemplateLoader {
* Add a new theme manifest for a given identifier. E.g. '$default' * Add a new theme manifest for a given identifier. E.g. '$default'
* *
* @param string $set * @param string $set
* @param ThemeManifest $manifest * @param ThemeList $manifest
*/ */
public function addSet($set, $manifest) { public function addSet($set, ThemeList $manifest) {
$this->sets[$set] = $manifest; $this->sets[$set] = $manifest;
} }
/**
* Get a named theme set
*
* @param string $set
* @return ThemeList
*/
public function getSet($set) {
if(isset($this->sets[$set])) {
return $this->sets[$set];
}
return null;
}
/** /**
* Given a theme identifier, determine the path from the root directory * Given a theme identifier, determine the path from the root directory
* *
@ -56,7 +82,7 @@ class TemplateLoader {
* of that module. ('/mymodule'). * of that module. ('/mymodule').
* *
* @param string $identifier Theme identifier. * @param string $identifier Theme identifier.
* @return string Path from root, not including leading forward slash. E.g. themes/mytheme * @return string Path from root, not including leading or trailing forward slash. E.g. themes/mytheme
*/ */
public function getPath($identifier) { public function getPath($identifier) {
$slashPos = strpos($identifier, '/'); $slashPos = strpos($identifier, '/');
@ -111,7 +137,9 @@ class TemplateLoader {
* theme-coupled resolution. * theme-coupled resolution.
* @param array $themes List of themes to use to resolve themes. In most cases * @param array $themes List of themes to use to resolve themes. In most cases
* you should pass in {@see SSViewer::get_themes()} * you should pass in {@see SSViewer::get_themes()}
* @return string Path to resolved template file, or null if not resolved. * @return string Absolute path to resolved template file, or null if not resolved.
* File location will be in the format themes/<theme>/templates/<directories>/<type>/<basename>.ss
* Note that type (e.g. 'Layout') is not the root level directory under 'templates'.
*/ */
public function findTemplate($template, $themes) { public function findTemplate($template, $themes) {
$type = ''; $type = '';
@ -141,14 +169,13 @@ class TemplateLoader {
$tail = array_pop($parts); $tail = array_pop($parts);
$head = implode('/', $parts); $head = implode('/', $parts);
foreach($themes as $themename) { $themePaths = $this->getThemePaths($themes);
$subthemes = isset($this->sets[$themename]) ? $this->sets[$themename]->getThemes() : [$themename]; foreach($themePaths as $themePath) {
// Join path
foreach($subthemes as $theme) { $pathParts = [ $this->base, $themePath, 'templates', $head, $type, $tail ];
$themePath = $this->base . '/' . $this->getPath($theme); $path = implode('/', array_filter($pathParts)) . '.ss';
if (file_exists($path)) {
$path = $themePath . '/templates/' . implode('/', array_filter([$head, $type, $tail])) . '.ss'; return $path;
if (file_exists($path)) return $path;
} }
} }
} }
@ -157,4 +184,73 @@ class TemplateLoader {
return null; return null;
} }
/**
* Resolve themed CSS path
*
* @param string $name Name of CSS file without extension
* @param array $themes List of themes
* @return string Path to resolved CSS file (relative to base dir)
*/
public function findThemedCSS($name, $themes)
{
$css = "/css/$name.css";
$paths = $this->getThemePaths($themes);
foreach ($paths as $themePath) {
$abspath = $this->base . '/' . $themePath;
if (file_exists($abspath . $css)) {
return $themePath . $css;
}
}
// CSS exists in no context
return null;
}
/**
* Registers the given themeable javascript as required.
*
* A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
* and it that doesn't exist and the module parameter is set then a javascript file with that name in
* the module is used.
*
* @param string $name The name of the file - eg '/js/File.js' would have the name 'File'
* @param array $themes List of themes
* @return string Path to resolved javascript file (relative to base dir)
*/
public function findThemedJavascript($name, $themes) {
$js = "/javascript/$name.js";
$paths = $this->getThemePaths($themes);
foreach ($paths as $themePath) {
$abspath = $this->base . '/' . $themePath;
if (file_exists($abspath . $js)) {
return $themePath . $js;
}
}
// js exists in no context
return null;
}
/**
* Resolve all themes to the list of root folders relative to site root
*
* @param array $themes List of themes to resolve. Supports named theme sets.
* @return array List of root-relative folders in order of precendence.
*/
public function getThemePaths($themes) {
$paths = [];
foreach($themes as $themename) {
// Expand theme sets
$set = $this->getSet($themename);
$subthemes = $set ? $set->getThemes() : [$themename];
// Resolve paths
foreach ($subthemes as $theme) {
$paths[] = $this->getPath($theme);
}
}
return $paths;
}
} }