Merge pull request #5804 from open-sausages/feature/themestack

API Theme stacking
This commit is contained in:
Sam Minnée 2016-07-15 17:12:54 +12:00 committed by GitHub
commit 5c98d331a3
68 changed files with 503 additions and 862 deletions

View File

@ -1509,7 +1509,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
) )
); );
$dateFormatField->setValue($self->DateFormat); $dateFormatField->setValue($self->DateFormat);
$dateFormatField->setDescriptionTemplate('MemberDatetimeOptionsetField_description_date'); $dateFormatField->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_date');
$defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale)); $defaultTimeFormat = Zend_Locale_Format::getTimeFormat(new Zend_Locale($self->Locale));
$timeFormatMap = array( $timeFormatMap = array(
@ -1526,7 +1526,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
) )
); );
$timeFormatField->setValue($self->TimeFormat); $timeFormatField->setValue($self->TimeFormat);
$timeFormatField->setDescriptionTemplate('MemberDatetimeOptionsetField_description_time'); $timeFormatField->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_time');
}); });
return parent::getCMSFields(); return parent::getCMSFields();

View File

@ -783,7 +783,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
return $controller->renderWith($controller->getTemplatesWithSuffix('_Content')); return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
}, },
'Breadcrumbs' => function() use (&$controller) { 'Breadcrumbs' => function() use (&$controller) {
return $controller->renderWith('CMSBreadcrumbs'); return $controller->renderWith('Includes/CMSBreadcrumbs');
}, },
'default' => function() use(&$controller) { 'default' => function() use(&$controller) {
return $controller->renderWith($controller->getViewer('show')); return $controller->renderWith($controller->getViewer('show'));

View File

@ -366,7 +366,7 @@ abstract class ModelAdmin extends LeftAndMain {
'ModelName' => Convert::raw2att($modelName), 'ModelName' => Convert::raw2att($modelName),
'Fields' => $specFields, 'Fields' => $specFields,
'Relations' => $specRelations, 'Relations' => $specRelations,
))->renderWith('ModelAdmin_ImportSpec'); ))->renderWith('Includes/ModelAdmin_ImportSpec');
$fields->push(new LiteralField("SpecFor{$modelName}", $specHTML)); $fields->push(new LiteralField("SpecFor{$modelName}", $specHTML));
$fields->push( $fields->push(

View File

@ -77,8 +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 'core/manifest/TemplateLoader.php'; require_once 'view/TemplateLoader.php';
require_once 'core/manifest/TemplateManifest.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';
@ -113,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
SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest( SilverStripe\View\TemplateLoader::instance()->addSet('$default', new SilverStripe\View\ThemeManifest(
BASE_PATH, project(), false, $flush BASE_PATH, project(), false, $flush
)); ));

View File

@ -1,87 +0,0 @@
<?php
/**
* Handles finding templates from a stack of template manifest objects.
*
* @package framework
* @subpackage manifest
*/
class SS_TemplateLoader {
/**
* @var SS_TemplateLoader
*/
private static $instance;
/**
* @var SS_TemplateManifest[]
*/
protected $manifests = array();
/**
* @return SS_TemplateLoader
*/
public static function instance() {
return self::$instance ? self::$instance : self::$instance = new self();
}
/**
* Returns the currently active template manifest instance.
*
* @return SS_TemplateManifest
*/
public function getManifest() {
return $this->manifests[count($this->manifests) - 1];
}
/**
* @param SS_TemplateManifest $manifest
*/
public function pushManifest(SS_TemplateManifest $manifest) {
$this->manifests[] = $manifest;
}
/**
* @return SS_TemplateManifest
*/
public function popManifest() {
return array_pop($this->manifests);
}
/**
* Attempts to find possible candidate templates from a set of template
* names from modules, current theme directory and finally the application
* folder.
*
* The template names can be passed in as plain strings, or be in the
* format "type/name", where type is the type of template to search for
* (e.g. Includes, Layout).
*
* @param string|array $templates
* @param string $theme
*
* @return array
*/
public function findTemplates($templates, $theme = null) {
$result = array();
foreach ((array) $templates as $template) {
if (strpos($template, '/')) {
list($type, $template) = explode('/', $template, 2);
} else {
$type = null;
}
if ($found = $this->getManifest()->getCandidateTemplate($template, $theme)) {
if ($type && isset($found[$type])) {
$found = array(
'main' => $found[$type]
);
}
$result = array_merge($found, $result);
}
}
return $result;
}
}

View File

@ -1,271 +0,0 @@
<?php
/**
* A class which builds a manifest of all templates present in a directory,
* in both modules and themes.
*
* @package framework
* @subpackage manifest
*/
class SS_TemplateManifest {
const TEMPLATES_DIR = 'templates';
protected $base;
protected $tests;
protected $cache;
protected $cacheKey;
protected $project;
protected $inited;
protected $templates = array();
/**
* Constructs a new template manifest. The manifest is not actually built
* or loaded from cache until needed.
*
* @param string $base The base path.
* @param string $project Path to application code
*
* @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function __construct($base, $project, $includeTests = false, $forceRegen = false) {
$this->base = $base;
$this->tests = $includeTests;
$this->project = $project;
$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
$this->cache = new $cacheClass('templatemanifest'.($includeTests ? '_tests' : ''));
$this->cacheKey = $this->getCacheKey($includeTests);
if ($forceRegen) {
$this->regenerate();
}
}
/**
* @return string
*/
public function getBase() {
return $this->base;
}
/**
* Generate a unique cache key to avoid manifest cache collisions.
* We compartmentalise based on the base path, the given project, and whether
* or not we intend to include tests.
* @param boolean $includeTests
* @return string
*/
public function getCacheKey($includeTests = false) {
return sha1(sprintf(
"manifest-%s-%s-%s",
$this->base,
$this->project,
(int) $includeTests // cast true to 1, false to 0
)
);
}
/**
* Returns a map of all template information. The map is in the following
* format:
*
* <code>
* array(
* 'moduletemplate' => array(
* 'main' => '/path/to/module/templates/Main.ss'
* ),
* 'include' => array(
* 'include' => '/path/to/module/templates/Includes/Include.ss'
* ),
* 'page' => array(
* 'themes' => array(
* 'simple' => array(
* 'main' => '/path/to/theme/Page.ss'
* 'Layout' => '/path/to/theme/Layout/Page.ss'
* )
* )
* )
* )
* </code>
*
* @return array
*/
public function getTemplates() {
if (!$this->inited) {
$this->init();
}
return $this->templates;
}
/**
* Returns a set of possible candidate templates that match a certain
* template name.
*
* This is the same as extracting an individual array element from
* {@link SS_TemplateManifest::getTemplates()}.
*
* @param string $name
* @return array
*/
public function getTemplate($name) {
if (!$this->inited) {
$this->init();
}
$name = strtolower($name);
if (array_key_exists($name, $this->templates)) {
return $this->templates[$name];
} else {
return array();
}
}
/**
* Returns the correct candidate template. In order of importance, application
* project code, current theme and finally modules.
*
* @param string $name
* @param string $theme - theme name
*
* @return array
*/
public function getCandidateTemplate($name, $theme = null) {
$found = array();
$candidates = $this->getTemplate($name);
// theme overrides modules
if ($theme && isset($candidates['themes'][$theme])) {
$found = array_merge($candidates, $candidates['themes'][$theme]);
}
// project overrides theme
if ($this->project && isset($candidates[$this->project])) {
$found = array_merge($found, $candidates[$this->project]);
}
$found = ($found) ? $found : $candidates;
if (isset($found['themes'])) unset($found['themes']);
if (isset($found[$this->project])) unset($found[$this->project]);
return $found;
}
/**
* Regenerates the manifest by scanning the base path.
*
* @param bool $cache
*/
public function regenerate($cache = true) {
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'name_regex' => '/\.ss$/',
'include_themes' => true,
'ignore_tests' => !$this->tests,
'file_callback' => array($this, 'handleFile')
));
$finder->find($this->base);
if ($cache) {
$this->cache->save($this->templates, $this->cacheKey);
}
$this->inited = true;
}
public function handleFile($basename, $pathname, $depth)
{
$projectFile = false;
$theme = null;
// Template in theme
if (preg_match(
'#'.preg_quote($this->base.'/'.THEMES_DIR).'/([^/_]+)(_[^/]+)?/(.*)$#',
$pathname,
$matches
)) {
$theme = $matches[1];
$relPath = $matches[3];
// Template in project
} elseif (preg_match(
'#'.preg_quote($this->base.'/'.$this->project).'/(.*)$#',
$pathname,
$matches
)) {
$projectFile = true;
$relPath = $matches[1];
// Template in module
} elseif (preg_match(
'#'.preg_quote($this->base).'/([^/]+)/(.*)$#',
$pathname,
$matches
)) {
$relPath = $matches[2];
} else {
throw new \LogicException("Can't determine meaning of path: $pathname");
}
// If a templates subfolder is used, ignore that
if (preg_match('#'.preg_quote(self::TEMPLATES_DIR).'/(.*)$#', $relPath, $matches)) {
$relPath = $matches[1];
}
// Layout and Content folders have special meaning
if (preg_match('#^(.*/)?(Layout|Content|Includes)/([^/]+)$#', $relPath, $matches)) {
$type = $matches[2];
$relPath = "$matches[1]$matches[3]";
} else {
$type = "main";
}
$name = strtolower(substr($relPath, 0, -3));
$name = str_replace('/', '\\', $name);
if ($theme) {
$this->templates[$name]['themes'][$theme][$type] = $pathname;
} else if ($projectFile) {
$this->templates[$name][$this->project][$type] = $pathname;
} else {
$this->templates[$name][$type] = $pathname;
}
// If we've found a template in a subdirectory, then allow its use for a non-namespaced class
// as well. This was a common SilverStripe 3 approach, where templates were placed into
// subfolders to suit the whim of the developer.
if (strpos($name, '\\') !== false) {
$name2 = substr($name, strrpos($name, '\\') + 1);
// In of these cases, the template will only be provided if it isn't already set. This
// matches SilverStripe 3 prioritisation.
if ($theme) {
if (!isset($this->templates[$name2]['themes'][$theme][$type])) {
$this->templates[$name2]['themes'][$theme][$type] = $pathname;
}
} else if ($projectFile) {
if (!isset($this->templates[$name2][$this->project][$type])) {
$this->templates[$name2][$this->project][$type] = $pathname;
}
} else {
if (!isset($this->templates[$name2][$type])) {
$this->templates[$name2][$type] = $pathname;
}
}
}
}
protected function init() {
if ($data = $this->cache->load($this->cacheKey)) {
$this->templates = $data;
$this->inited = true;
} else {
$this->regenerate();
}
}
}

View File

@ -13,8 +13,8 @@ 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\ThemeManifest;
/** /**
* Test case class for the Sapphire framework. * Test case class for the Sapphire framework.
@ -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);
SS_TemplateLoader::instance()->pushManifest(new SS_TemplateManifest( TemplateLoader::instance()->addSet('$default', new ThemeManifest(
BASE_PATH, project(), true, $flush BASE_PATH, project(), true, $flush
)); ));
@ -1054,22 +1054,15 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
*/ */
protected function useTestTheme($themeBaseDir, $theme, $callback) { protected function useTestTheme($themeBaseDir, $theme, $callback) {
Config::nest(); Config::nest();
global $project;
$manifest = new SS_TemplateManifest($themeBaseDir, $project, true, true); if (strpos($themeBaseDir, BASE_PATH) === 0) $themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
SS_TemplateLoader::instance()->pushManifest($manifest);
Config::inst()->update('SSViewer', 'theme', $theme);
$e = null; $e = null;
try { $callback(); } try { $callback(); }
catch (Exception $e) { /* NOP for now, just save $e */ } catch (Exception $e) { /* NOP for now, just save $e */ }
// Remove all the test themes we created
SS_TemplateLoader::instance()->popManifest();
Config::unnest(); Config::unnest();
if ($e) throw $e; if ($e) throw $e;

View File

@ -397,6 +397,14 @@ all changed project files.
This will resolve the majority of upgrading work, but for specific changes that will This will resolve the majority of upgrading work, but for specific changes that will
require manual intervention, please see the below upgrading notes. require manual intervention, please see the below upgrading notes.
### Make sure templates are in correct locations
Templates are now much more strict about their locations. You can no longer put a template in an arbitrary
folder and have it be found. Case is now also checked on case-sensitive filesystems.
Either include the folder in the template name (`renderWith('Field.ss')` => `renderWith('forms/Field.ss')`), move the
template into the correct directory, or both.
### Update code that uses SQLQuery ### Update code that uses SQLQuery
Where your code once used SQLQuery you should now use SQLSelect in all cases, as this has been removed. Where your code once used SQLQuery you should now use SQLSelect in all cases, as this has been removed.

View File

@ -373,7 +373,7 @@ class Email extends ViewableData {
// Requery data so that updated versions of To, From, Subject, etc are included // Requery data so that updated versions of To, From, Subject, etc are included
$data = $this->templateData(); $data = $this->templateData();
$template = new SSViewer($this->ss_template); $template = new SSViewer('email/'.$this->ss_template);
if($template->exists()) { if($template->exists()) {
$fullBody = $template->process($data); $fullBody = $template->process($data);

View File

@ -16,10 +16,10 @@ class ProtectedAssetAdapter extends AssetAdapter implements ProtectedAdapter {
private static $server_configuration = array( private static $server_configuration = array(
'apache' => array( 'apache' => array(
'.htaccess' => "Protected_HTAccess" '.htaccess' => "filesystem/Protected_HTAccess"
), ),
'microsoft-iis' => array( 'microsoft-iis' => array(
'web.config' => "Protected_WebConfig" 'web.config' => "filesystem/Protected_WebConfig"
) )
); );

View File

@ -21,10 +21,10 @@ class PublicAssetAdapter extends AssetAdapter implements PublicAdapter {
*/ */
private static $server_configuration = array( private static $server_configuration = array(
'apache' => array( 'apache' => array(
'.htaccess' => "Assets_HTAccess" '.htaccess' => "filesystem/Assets_HTAccess"
), ),
'microsoft-iis' => array( 'microsoft-iis' => array(
'web.config' => "Assets_WebConfig" 'web.config' => "filesystem/Assets_WebConfig"
) )
); );

View File

@ -48,7 +48,7 @@ class AssetField extends FileField {
* *
* @var string * @var string
*/ */
protected $templateFileButtons = 'AssetField_FileButtons'; protected $templateFileButtons = 'Includes/AssetField_FileButtons';
/** /**
* Parent data record. Will be infered from parent form or controller if blank. The destination * Parent data record. Will be infered from parent form or controller if blank. The destination

View File

@ -1632,7 +1632,7 @@ class Form extends RequestHandler {
public function forTemplate() { public function forTemplate() {
$return = $this->renderWith(array_merge( $return = $this->renderWith(array_merge(
(array)$this->getTemplate(), (array)$this->getTemplate(),
array('Form') array('Includes/Form')
)); ));
// Now that we're rendered, clear message // Now that we're rendered, clear message

View File

@ -1052,7 +1052,7 @@ class FormField extends RequestHandler {
$matches = array(); $matches = array();
foreach(array_reverse(ClassInfo::ancestry($this)) as $className) { foreach(array_reverse(ClassInfo::ancestry($this)) as $className) {
$matches[] = $className . $customTemplateSuffix; $matches[] = 'forms/'. $className . $customTemplateSuffix;
if($className == "FormField") { if($className == "FormField") {
break; break;
@ -1060,7 +1060,7 @@ class FormField extends RequestHandler {
} }
if($customTemplate) { if($customTemplate) {
array_unshift($matches, $customTemplate); array_unshift($matches, 'forms/'.$customTemplate);
} }
return $matches; return $matches;

View File

@ -44,6 +44,7 @@ class MemberDatetimeOptionsetField extends OptionsetField {
'Options' => new ArrayList($options) 'Options' => new ArrayList($options)
)); ));
return $this->customise($properties)->renderWith( return $this->customise($properties)->renderWith(
$this->getTemplates() $this->getTemplates()
); );

View File

@ -65,7 +65,7 @@ class UploadField extends FileField {
* *
* @var string * @var string
*/ */
protected $templateFileButtons = 'UploadField_FileButtons'; protected $templateFileButtons = 'Includes/UploadField_FileButtons';
/** /**
* Template to use for the edit form * Template to use for the edit form

View File

@ -131,7 +131,7 @@ class GridFieldAddExistingAutocompleter
} }
return array( return array(
$this->targetFragment => $forTemplate->renderWith($this->itemClass) $this->targetFragment => $forTemplate->renderWith('Includes/'.$this->itemClass)
); );
} }

View File

@ -44,7 +44,7 @@ class GridFieldAddNewButton implements GridField_HTMLProvider {
)); ));
return array( return array(
$this->targetFragment => $data->renderWith('GridFieldAddNewbutton'), $this->targetFragment => $data->renderWith('Includes/GridFieldAddNewButton'),
); );
} }

View File

@ -27,7 +27,7 @@ class GridFieldButtonRow implements GridField_HTMLProvider {
)); ));
return array( return array(
$this->targetFragment => $data->renderWith('GridFieldButtonRow') $this->targetFragment => $data->renderWith('Includes/GridFieldButtonRow')
); );
} }
} }

View File

@ -441,7 +441,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
// Always show with base template (full width, no other panels), // Always show with base template (full width, no other panels),
// regardless of overloaded CMS controller templates. // regardless of overloaded CMS controller templates.
// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller // TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
$form->setTemplate('LeftAndMain_EditForm'); $form->setTemplate('Includes/LeftAndMain_EditForm');
$form->addExtraClass('cms-content cms-edit-form center'); $form->addExtraClass('cms-content cms-edit-form center');
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content'); $form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
if($form->Fields()->hasTabset()) { if($form->Fields()->hasTabset()) {

View File

@ -86,7 +86,7 @@ class GridFieldEditButton implements GridField_ColumnProvider {
'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit') 'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit')
)); ));
return $data->renderWith('GridFieldEditButton'); return $data->renderWith('Includes/GridFieldEditButton');
} }
/** /**

View File

@ -162,7 +162,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
} }
return array( return array(
'header' => $forTemplate->renderWith('GridFieldFilterHeader_Row'), 'header' => $forTemplate->renderWith('Includes/GridFieldFilterHeader_Row'),
); );
} }
} }

View File

@ -48,7 +48,7 @@ class GridFieldFooter implements GridField_HTMLProvider {
return array( return array(
'footer' => $forTemplate->renderWith( 'footer' => $forTemplate->renderWith(
'GridFieldFooter', 'Includes/GridFieldFooter',
array( array(
'Colspan' => count($gridField->getColumns()) 'Colspan' => count($gridField->getColumns())
) )

View File

@ -66,7 +66,7 @@ class GridFieldLevelup extends Object implements GridField_HTMLProvider {
)); ));
return array( return array(
'before' => $forTemplate->renderWith('GridFieldLevelup'), 'before' => $forTemplate->renderWith('Includes/GridFieldLevelup'),
); );
} }
} }

View File

@ -67,7 +67,7 @@ class GridFieldPageCount implements GridField_HTMLProvider {
$paginator = $this->getPaginator($gridField); $paginator = $this->getPaginator($gridField);
if ($paginator && ($forTemplate = $paginator->getTemplateParameters($gridField))) { if ($paginator && ($forTemplate = $paginator->getTemplateParameters($gridField))) {
return array( return array(
$this->targetFragment => $forTemplate->renderWith($this->itemClass) $this->targetFragment => $forTemplate->renderWith('Includes/'.$this->itemClass)
); );
} }
} }

View File

@ -259,7 +259,7 @@ class GridFieldPaginator implements GridField_HTMLProvider, GridField_DataManipu
$forTemplate = $this->getTemplateParameters($gridField); $forTemplate = $this->getTemplateParameters($gridField);
if($forTemplate) { if($forTemplate) {
return array( return array(
'footer' => $forTemplate->renderWith($this->itemClass, 'footer' => $forTemplate->renderWith('Includes/'.$this->itemClass,
array('Colspan'=>count($gridField->getColumns()))) array('Colspan'=>count($gridField->getColumns())))
); );
} }

View File

@ -164,7 +164,7 @@ class GridFieldSortableHeader implements GridField_HTMLProvider, GridField_DataM
} }
return array( return array(
'header' => $forTemplate->renderWith('GridFieldSortableHeader_Row'), 'header' => $forTemplate->renderWith('Includes/GridFieldSortableHeader_Row'),
); );
} }

View File

@ -13,7 +13,7 @@ class GridFieldToolbarHeader implements GridField_HTMLProvider {
public function getHTMLFragments( $gridField) { public function getHTMLFragments( $gridField) {
return array( return array(
'header' => $gridField->renderWith('GridFieldToolbarHeader') 'header' => $gridField->renderWith('Includes/GridFieldToolbarHeader')
); );
} }
} }

View File

@ -22,7 +22,7 @@ class GridFieldViewButton implements GridField_ColumnProvider {
$data = new ArrayData(array( $data = new ArrayData(array(
'Link' => Controller::join_links($field->Link('item'), $record->ID, 'view') 'Link' => Controller::join_links($field->Link('item'), $record->ID, 'view')
)); ));
return $data->renderWith('GridFieldViewButton'); return $data->renderWith('Includes/GridFieldViewButton');
} }
} }

View File

@ -181,7 +181,7 @@ class HTMLEditorField_Toolbar extends RequestHandler {
/** /**
* @var string * @var string
*/ */
protected $templateViewFile = 'HTMLEditorField_viewfile'; protected $templateViewFile = 'Includes/HTMLEditorField_viewfile';
protected $controller, $name; protected $controller, $name;

View File

@ -1,5 +1,7 @@
<?php <?php
use SilverStripe\View\TemplateLoader;
/** /**
* Default configuration for HtmlEditor specific to tinymce * Default configuration for HtmlEditor specific to tinymce
*/ */
@ -419,13 +421,18 @@ class TinyMCEConfig extends HTMLEditorConfig {
Director::absoluteBaseURL(), Director::absoluteBaseURL(),
FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css' FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css'
); );
if($theme = SSViewer::get_theme_folder()) {
$editorDir = $theme . '/css/editor.css'; foreach(SSViewer::get_themes() as $theme) {
$path = TemplateLoader::instance()->getPath($theme);
$editorDir = $path . '/css/editor.css';;
if(file_exists(BASE_PATH . '/' . $editorDir)) { if(file_exists(BASE_PATH . '/' . $editorDir)) {
$editor[] = Controller::join_links( $editor[] = Controller::join_links(
Director::absoluteBaseURL(), Director::absoluteBaseURL(),
$editorDir $editorDir
); );
break;
} }
} }
return $editor; return $editor;

View File

@ -1,6 +1,10 @@
<?php <?php
use SilverStripe\View\TemplateLoader;
use SilverStripe\View\ThemeManifest;
/** /**
* Tests for the {@link SS_TemplateLoader} class. * Tests for the {@link TemplateLoader} class.
* *
* @package framework * @package framework
* @subpackage tests * @subpackage tests
@ -16,162 +20,82 @@ class TemplateLoaderTest extends SapphireTest {
*/ */
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
// Fake project root
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest'; $this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
$this->manifest = new SS_TemplateManifest($this->base, 'myproject', false, true); // New ThemeManifest for that root
$this->loader = new SS_TemplateLoader(); $this->manifest = new \SilverStripe\View\ThemeManifest($this->base, 'myproject', false, true);
$this->refreshLoader(); // New Loader for that root
$this->loader = new \SilverStripe\View\TemplateLoader($this->base);
$this->loader->addSet('$default', $this->manifest);
} }
/** /**
* Test that 'main' and 'Layout' templates are loaded from module * Test that 'main' and 'Layout' templates are loaded from module
*/ */
public function testFindTemplatesInModule() { public function testFindTemplatesInModule() {
$expect = array( $this->assertEquals(
'main' => "$this->base/module/templates/Page.ss", "$this->base/module/templates/Page.ss",
'Layout' => "$this->base/module/templates/Layout/Page.ss" $this->loader->findTemplate('Page', ['$default'])
);
$this->assertEquals(
"$this->base/module/templates/Layout/Page.ss",
$this->loader->findTemplate(['type' => 'Layout', 'Page'], ['$default'])
); );
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
} }
/** /**
* Test that 'main' and 'Layout' templates are loaded from set theme * Test that 'main' and 'Layout' templates are loaded from set theme
*/ */
public function testFindTemplatesInTheme() { public function testFindTemplatesInTheme() {
$expect = array( $this->assertEquals(
'main' => "$this->base/themes/theme/templates/Page.ss", "$this->base/themes/theme/templates/Page.ss",
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss" $this->loader->findTemplate('Page', ['theme'])
);
$this->assertEquals(
"$this->base/themes/theme/templates/Layout/Page.ss",
$this->loader->findTemplate(['type' => 'Layout', 'Page'], ['theme'])
); );
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE', 'theme'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page'), 'theme'));
} }
/** /**
* Test that 'main' and 'Layout' templates are loaded from project without a set theme * Test that 'main' and 'Layout' templates are loaded from project without a set theme
*/ */
public function testFindTemplatesInApplication() { public function testFindTemplatesInApplication() {
// TODO: replace with one that doesn't create temporary files (so bad)
$templates = array( $templates = array(
$this->base . '/myproject/templates/Page.ss', $this->base . '/myproject/templates/Page.ss',
$this->base . '/myproject/templates/Layout/Page.ss' $this->base . '/myproject/templates/Layout/Page.ss'
); );
$this->createTestTemplates($templates); $this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array( $this->assertEquals(
'main' => "$this->base/myproject/templates/Page.ss", "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss" $this->loader->findTemplate('Page', ['$default'])
);
$this->assertEquals(
"$this->base/myproject/templates/Layout/Page.ss",
$this->loader->findTemplate(['type' => 'Layout', 'Page'], ['$default'])
); );
$this->assertEquals($expect, $this->loader->findTemplates('Page'));
$this->assertEquals($expect, $this->loader->findTemplates('PAGE'));
$this->assertEquals($expect, $this->loader->findTemplates(array('Foo', 'Page')));
$this->removeTestTemplates($templates); $this->removeTestTemplates($templates);
} }
/**
* Test that 'Layout' template is loaded from module
*/
public function testFindTemplatesInModuleLayout() {
$expect = array(
'main' => "$this->base/module/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page'));
}
/**
* Test that 'Layout' template is loaded from theme
*/
public function testFindTemplatesInThemeLayout() {
$expect = array(
'main' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Layout/Page', 'theme'));
}
/** /**
* Test that 'main' template is found in theme and 'Layout' is found in module * Test that 'main' template is found in theme and 'Layout' is found in module
*/ */
public function testFindTemplatesMainThemeLayoutModule() { public function testFindTemplatesMainThemeLayoutModule() {
$expect = array( $this->assertEquals(
'main' => "$this->base/themes/theme/templates/CustomThemePage.ss", "$this->base/themes/theme/templates/CustomThemePage.ss",
'Layout' => "$this->base/module/templates/Layout/CustomThemePage.ss" $this->loader->findTemplate('CustomThemePage', ['theme', '$default'])
); );
$this->assertEquals($expect, $this->loader->findTemplates(array('CustomThemePage', 'Page'), 'theme'));
}
/** $this->assertEquals(
* Test that project template overrides module template of same name "$this->base/module/templates/Layout/CustomThemePage.ss",
*/ $this->loader->findTemplate(['type' => 'Layout', 'CustomThemePage'], ['theme', '$default'])
public function testFindTemplatesApplicationOverridesModule() {
$expect = array(
'main' => "$this->base/myproject/templates/CustomTemplate.ss"
); );
$this->assertEquals($expect, $this->loader->findTemplates('CustomTemplate'));
}
/**
* Test that project templates overrides theme templates
*/
public function testFindTemplatesApplicationOverridesTheme() {
$templates = array(
$this->base . '/myproject/templates/Page.ss',
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page'), 'theme');
$this->removeTestTemplates($templates);
}
/**
* Test that project 'Layout' template overrides theme 'Layout' template
*/
public function testFindTemplatesApplicationLayoutOverridesThemeLayout() {
$templates = array(
$this->base . '/myproject/templates/Layout/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/themes/theme/templates/Page.ss",
'Layout' => "$this->base/myproject/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->removeTestTemplates($templates);
}
/**
* Test that project 'main' template overrides theme 'main' template
*/
public function testFindTemplatesApplicationMainOverridesThemeMain() {
$templates = array(
$this->base . '/myproject/templates/Page.ss'
);
$this->createTestTemplates($templates);
$this->refreshLoader();
$expect = array(
'main' => "$this->base/myproject/templates/Page.ss",
'Layout' => "$this->base/themes/theme/templates/Layout/Page.ss"
);
$this->assertEquals($expect, $this->loader->findTemplates('Page', 'theme'));
$this->removeTestTemplates($templates);
}
protected function refreshLoader() {
$this->manifest->regenerate(false);
$this->loader->pushManifest($this->manifest);
} }
protected function createTestTemplates($templates) { protected function createTestTemplates($templates) {

View File

@ -1,141 +0,0 @@
<?php
/**
* Tests for the template manifest.
*
* @package framework
* @subpackage tests
*/
class TemplateManifestTest extends SapphireTest {
protected $base;
protected $manifest;
protected $manifestTests;
public function setUp() {
parent::setUp();
$this->base = dirname(__FILE__) . '/fixtures/templatemanifest';
$this->manifest = new SS_TemplateManifest($this->base, 'myproject');
$this->manifestTests = new SS_TemplateManifest($this->base, 'myproject', true);
$this->manifest->regenerate(false);
$this->manifestTests->regenerate(false);
}
public function testGetTemplates() {
$expect = array(
'root' => array(
'main' => "{$this->base}/module/Root.ss"
),
'page' => array(
'main' => "{$this->base}/module/templates/Page.ss",
'Layout' => "{$this->base}/module/templates/Layout/Page.ss",
'themes' => array('theme' => array(
'main' => "{$this->base}/themes/theme/templates/Page.ss",
'Layout' => "{$this->base}/themes/theme/templates/Layout/Page.ss"
))
),
'custompage' => array(
'Layout' => "{$this->base}/module/templates/Layout/CustomPage.ss"
),
'customtemplate' => array(
'main' => "{$this->base}/module/templates/CustomTemplate.ss",
'myproject' => array(
'main' => "{$this->base}/myproject/templates/CustomTemplate.ss"
)
),
'subfolder' => array(
'main' => "{$this->base}/module/subfolder/templates/Subfolder.ss"
),
'customthemepage' => array (
'Layout' => "{$this->base}/module/templates/Layout/CustomThemePage.ss",
'themes' =>
array(
'theme' => array('main' => "{$this->base}/themes/theme/templates/CustomThemePage.ss",)
)
),
'mynamespace\myclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MyClass.ss",
'Layout' => "{$this->base}/module/templates/MyNamespace/Layout/MyClass.ss",
'themes' => array(
'theme' => array(
'main' => "{$this->base}/themes/theme/templates/MyNamespace/MyClass.ss",
)
),
),
'mynamespace\mysubnamespace\mysubclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MySubnamespace/MySubclass.ss",
),
'myclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MyClass.ss",
'Layout' => "{$this->base}/module/templates/MyNamespace/Layout/MyClass.ss",
'themes' => array(
'theme' => array(
'main' => "{$this->base}/themes/theme/templates/MyNamespace/MyClass.ss",
)
),
),
'mysubclass' => array(
'main' => "{$this->base}/module/templates/MyNamespace/MySubnamespace/MySubclass.ss",
),
'include' => array('themes' => array(
'theme' => array(
'Includes' => "{$this->base}/themes/theme/templates/Includes/Include.ss"
)
))
);
$expectTests = $expect;
$expectTests['test'] = array(
'main' => "{$this->base}/module/tests/templates/Test.ss"
);
$manifest = $this->manifest->getTemplates();
$manifestTests = $this->manifestTests->getTemplates();
ksort($expect);
ksort($expectTests);
ksort($manifest);
ksort($manifestTests);
$this->assertEquals(
$expect, $manifest,
'All templates are correctly loaded in the manifest.'
);
$this->assertEquals(
$expectTests, $manifestTests,
'The test manifest is the same, but includes test templates.'
);
}
public function testGetTemplate() {
$expectPage = array(
'main' => "{$this->base}/module/templates/Page.ss",
'Layout' => "{$this->base}/module/templates/Layout/Page.ss",
'themes' => array('theme' => array(
'main' => "{$this->base}/themes/theme/templates/Page.ss",
'Layout' => "{$this->base}/themes/theme/templates/Layout/Page.ss"
))
);
$expectTests = array(
'main' => "{$this->base}/module/tests/templates/Test.ss"
);
$this->assertEquals($expectPage, $this->manifest->getTemplate('Page'));
$this->assertEquals($expectPage, $this->manifest->getTemplate('PAGE'));
$this->assertEquals($expectPage, $this->manifestTests->getTemplate('Page'));
$this->assertEquals($expectPage, $this->manifestTests->getTemplate('PAGE'));
$this->assertEquals(array(), $this->manifest->getTemplate('Test'));
$this->assertEquals($expectTests, $this->manifestTests->getTemplate('Test'));
$this->assertEquals(array(
'main' => "{$this->base}/module/templates/CustomTemplate.ss",
'myproject' => array(
'main' => "{$this->base}/myproject/templates/CustomTemplate.ss"
)), $this->manifestTests->getTemplate('CustomTemplate'));
}
}

View File

@ -110,7 +110,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest {
$field->setDescription('Test description'); $field->setDescription('Test description');
$this->assertEquals('Test description', $field->getDescription()); $this->assertEquals('Test description', $field->getDescription());
$field->setDescriptionTemplate('MemberDatetimeOptionsetField_description_time'); $field->setDescriptionTemplate('forms/MemberDatetimeOptionsetField_description_time');
$this->assertNotEmpty($field->getDescription()); $this->assertNotEmpty($field->getDescription());
$this->assertNotEquals('Test description', $field->getDescription()); $this->assertNotEquals('Test description', $field->getDescription());
} }

View File

@ -1,9 +1,11 @@
<?php <?php
use SilverStripe\View\TemplateLoader;
/** /**
* @package framework * @package framework
* @subpackage i18n * @subpackage i18n
*/ */
class i18nSSLegacyAdapterTest extends SapphireTest { class i18nSSLegacyAdapterTest extends SapphireTest {
public function setUp() { public function setUp() {
@ -14,10 +16,10 @@ class i18nSSLegacyAdapterTest extends SapphireTest {
Filesystem::makeFolder($this->alternateBaseSavePath); Filesystem::makeFolder($this->alternateBaseSavePath);
Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath); Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath);
// Push a template loader running from the fake webroot onto the stack. // Replace old template loader with new one with alternate base path
$templateManifest = new SS_TemplateManifest($this->alternateBasePath, null, false, true); $this->_oldLoader = TemplateLoader::instance();
$templateManifest->regenerate(false); TemplateLoader::set_instance(new TemplateLoader($this->alternateBasePath));
SS_TemplateLoader::instance()->pushManifest($templateManifest);
$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');
@ -40,7 +42,7 @@ class i18nSSLegacyAdapterTest extends SapphireTest {
} }
public function tearDown() { public function tearDown() {
SS_TemplateLoader::instance()->popManifest(); TemplateLoader::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,6 +1,9 @@
<?php <?php
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\View\TemplateLoader;
use SilverStripe\View\ThemeManifest;
require_once 'Zend/Translate.php'; require_once 'Zend/Translate.php';
/** /**
@ -35,10 +38,13 @@ class i18nTest extends SapphireTest {
FileSystem::makeFolder($this->alternateBaseSavePath); FileSystem::makeFolder($this->alternateBaseSavePath);
Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath); Config::inst()->update('Director', 'alternate_base_folder', $this->alternateBasePath);
// Push a template loader running from the fake webroot onto the stack. // Replace old template loader with new one with alternate base path
$templateManifest = new SS_TemplateManifest($this->alternateBasePath, null, false, true); $this->_oldLoader = TemplateLoader::instance();
$templateManifest->regenerate(false); TemplateLoader::set_instance($loader = new TemplateLoader($this->alternateBasePath));
SS_TemplateLoader::instance()->pushManifest($templateManifest); $loader->addSet('$default', new ThemeManifest(
$this->alternateBasePath, project(), false, true
));
$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');
@ -58,7 +64,7 @@ class i18nTest extends SapphireTest {
} }
public function tearDown() { public function tearDown() {
SS_TemplateLoader::instance()->popManifest(); TemplateLoader::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,4 +1,7 @@
<?php <?php
use SilverStripe\View\TemplateLoader;
/** /**
* @package framework * @package framework
* @subpackage tests * @subpackage tests
@ -34,13 +37,13 @@ class i18nTextCollectorTest extends SapphireTest {
$this->alternateBasePath, false, true, false $this->alternateBasePath, false, true, false
); );
$manifest = new SS_TemplateManifest($this->alternateBasePath, null, false, true); // Replace old template loader with new one with alternate base path
$manifest->regenerate(false); $this->_oldLoader = TemplateLoader::instance();
SS_TemplateLoader::instance()->pushManifest($manifest); TemplateLoader::set_instance(new TemplateLoader($this->alternateBasePath));
} }
public function tearDown() { public function tearDown() {
SS_TemplateLoader::instance()->popManifest(); TemplateLoader::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

@ -2,6 +2,5 @@
<% include SSViewerTestProcessHead %> <% include SSViewerTestProcessHead %>
<body> <body>
<% include SSViewerTestCommentsWithInclude %>
</body> </body>
</html> </html>

View File

@ -759,17 +759,11 @@ after')
$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data), $this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
'Forward slashes work for namespace references in includes' 'Forward slashes work for namespace references in includes'
); );
$this->assertEquals(
"tests:( NamespaceInclude\n )",
$this->render('tests:( <% include NamespaceInclude %> )', $data),
'Namespace can be missed for a namespaed include'
);
} }
public function testRecursiveInclude() { public function testRecursiveInclude() {
$view = new SSViewer(array('SSViewerTestRecursiveInclude')); $view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
$data = new ArrayData(array( $data = new ArrayData(array(
'Title' => 'A', 'Title' => 'A',
@ -1160,21 +1154,21 @@ after')
$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) { $this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
// Test passing a string // Test passing a string
$templates = SSViewer::get_templates_by_class( $templates = SSViewer::get_templates_by_class(
'TestNamespace\SSViewerTest_Controller', 'TestNamespace\SSViewerTestModel_Controller',
'', '',
'Controller' 'Controller'
); );
$self->assertEquals([ $self->assertEquals([
'TestNamespace\SSViewerTest_Controller', 'TestNamespace\SSViewerTestModel_Controller',
'Controller', 'Controller',
], $templates); ], $templates);
// Test to ensure we're stopping at the base class. // Test to ensure we're stopping at the base class.
$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTest_Controller', '', 'TestNamespace\SSViewerTest_Controller'); $templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel_Controller', '', 'TestNamespace\SSViewerTestModel_Controller');
$self->assertCount(1, $templates); $self->assertCount(1, $templates);
// Make sure we can filter our templates by suffix. // Make sure we can filter our templates by suffix.
$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller'); $templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTestModel', '_Controller');
$self->assertCount(1, $templates); $self->assertCount(1, $templates);
// Let's throw something random in there. // Let's throw something random in there.
@ -1183,38 +1177,6 @@ after')
}); });
} }
/**
* @covers SSViewer::get_themes()
*/
public function testThemeRetrieval() {
$ds = DIRECTORY_SEPARATOR;
$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir);
mkdir($testThemeBaseDir);
mkdir($testThemeBaseDir . $ds . 'blackcandy');
mkdir($testThemeBaseDir . $ds . 'blackcandy_blog');
mkdir($testThemeBaseDir . $ds . 'darkshades');
mkdir($testThemeBaseDir . $ds . 'darkshades_blog');
$this->assertEquals(array(
'blackcandy' => 'blackcandy',
'darkshades' => 'darkshades'
), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes');
$this->assertEquals(array(
'blackcandy' => 'blackcandy',
'blackcandy_blog' => 'blackcandy_blog',
'darkshades' => 'darkshades',
'darkshades_blog' => 'darkshades_blog'
), SSViewer::get_themes($testThemeBaseDir, true),
'Our test theme directory contains 2 themes and 2 sub-themes');
// Remove all the test themes we created
Filesystem::removeFolder($testThemeBaseDir);
}
public function testRewriteHashlinks() { public function testRewriteHashlinks() {
$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links'); $orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
Config::inst()->update('SSViewer', 'rewrite_hash_links', true); Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
@ -1327,6 +1289,7 @@ EOC;
$origEnv = Config::inst()->get('Director', 'environment_type'); $origEnv = Config::inst()->get('Director', 'environment_type');
Config::inst()->update('Director', 'environment_type', 'dev'); Config::inst()->update('Director', 'environment_type', 'dev');
Config::inst()->update('SSViewer', 'source_file_comments', true); Config::inst()->update('SSViewer', 'source_file_comments', true);
$i = FRAMEWORK_PATH . '/tests/templates/Includes';
$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments'; $f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
$templates = array( $templates = array(
array( array(
@ -1398,16 +1361,16 @@ EOC;
"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->" "<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
. "<div class='typography'>" . "<div class='typography'>"
. "<!-- include 'SSViewerTestCommentsInclude' -->" . "<!-- include 'SSViewerTestCommentsInclude' -->"
. "<!-- template $f/SSViewerTestCommentsInclude.ss -->" . "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
. "Included" . "Included"
. "<!-- end template $f/SSViewerTestCommentsInclude.ss -->" . "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
. "<!-- end include 'SSViewerTestCommentsInclude' -->" . "<!-- end include 'SSViewerTestCommentsInclude' -->"
. "</div>" . "</div>"
. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->", . "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
), ),
); );
foreach ($templates as $template) { foreach ($templates as $template) {
$this->_renderWithSourceFileComments($template['name'], $template['expected']); $this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
} }
Config::inst()->update('SSViewer', 'source_file_comments', false); Config::inst()->update('SSViewer', 'source_file_comments', false);
Config::inst()->update('Director', 'environment_type', $origEnv); Config::inst()->update('Director', 'environment_type', $origEnv);

View File

@ -2,7 +2,12 @@
namespace TestNamespace; namespace TestNamespace;
class SSViewerTest_Controller extends \Controller use SilverStripe\ORM\DataObject;
{
class SSViewerTestModel extends DataObject {
}
class SSViewerTestModel_Controller extends \Controller {
} }

View File

@ -1,6 +1,7 @@
<?php <?php
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler; use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
use SilverStripe\View\TemplateLoader;
/** /**
* Requirements tracker for JavaScript and CSS. * Requirements tracker for JavaScript and CSS.
@ -818,7 +819,7 @@ class Requirements_Backend
public function javascript($file, $options = array()) { public function javascript($file, $options = array()) {
// make sure that async/defer is set if it is set once even if file is included multiple times // make sure that async/defer is set if it is set once even if file is included multiple times
$async = ( $async = (
isset($options['async']) && isset($options['async']) == true isset($options['async']) && isset($options['async']) == true
|| ( || (
isset($this->javascript[$file]) isset($this->javascript[$file])
&& isset($this->javascript[$file]['async']) && isset($this->javascript[$file]['async'])
@ -842,7 +843,7 @@ class Requirements_Backend
if(isset($options['provides'])) { if(isset($options['provides'])) {
$this->providedJavascript[$file] = array_values($options['provides']); $this->providedJavascript[$file] = array_values($options['provides']);
} }
} }
/** /**
@ -1798,21 +1799,27 @@ class Requirements_Backend
* (e.g. 'screen,projector') * (e.g. 'screen,projector')
*/ */
public function themedCSS($name, $module = null, $media = null) { public function themedCSS($name, $module = null, $media = null) {
$theme = SSViewer::get_theme_folder();
$project = project();
$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
$abstheme = $absbase . $theme;
$absproject = $absbase . $project;
$css = "/css/$name.css"; $css = "/css/$name.css";
$project = project();
$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
$absproject = $absbase . $project;
if(file_exists($absproject . $css)) { if(file_exists($absproject . $css)) {
$this->css($project . $css, $media); return $this->css($project . $css, $media);
} elseif($module && file_exists($abstheme . '_' . $module.$css)) { }
$this->css($theme . '_' . $module . $css, $media);
} elseif(file_exists($abstheme . $css)) { foreach(SSViewer::get_themes() as $theme) {
$this->css($theme . $css, $media); $path = TemplateLoader::instance()->getPath($theme);
} elseif($module) { $abspath = BASE_PATH . '/' . $path;
$this->css($module . $css, $media);
if(file_exists($abspath . $css)) {
return $this->css($path . $css, $media);
}
}
if($module) {
return $this->css($module . $css, $media);
} }
} }

View File

@ -3382,7 +3382,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$template = $res['template']; $template = $res['template'];
$arguments = $res['arguments']; $arguments = $res['arguments'];
$res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . $res['php'] = '$val .= SSViewer::execute_template(["type" => "Includes", '.$template.'], $scope->getItem(), array(' .
implode(',', $arguments)."), \$scope);\n"; implode(',', $arguments)."), \$scope);\n";
if($this->includeDebuggingComments) { // Add include filename comments on dev sites if($this->includeDebuggingComments) { // Add include filename comments on dev sites

View File

@ -830,7 +830,7 @@ class SSTemplateParser extends Parser implements TemplateParser {
$template = $res['template']; $template = $res['template'];
$arguments = $res['arguments']; $arguments = $res['arguments'];
$res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . $res['php'] = '$val .= SSViewer::execute_template(["type" => "Includes", '.$template.'], $scope->getItem(), array(' .
implode(',', $arguments)."), \$scope);\n"; implode(',', $arguments)."), \$scope);\n";
if($this->includeDebuggingComments) { // Add include filename comments on dev sites if($this->includeDebuggingComments) { // Add include filename comments on dev sites

View File

@ -3,6 +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;
/** /**
* 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:
@ -723,10 +724,19 @@ class SSViewer implements Flushable {
} }
/** /**
* @var array $chosenTemplates Associative array for the different * @var array $templates List of templates to select from
* template containers: "main" and "Layout". Values are absolute file paths to *.ss files.
*/ */
private $chosenTemplates = array(); private $templates = null;
/**
* @var string $chosen Absolute path to chosen template file
*/
private $chosen = null;
/**
* @var array Templates to use when looking up 'Layout' or 'Content'
*/
private $subTemplates = null;
/** /**
* @var boolean * @var boolean
@ -735,9 +745,17 @@ class SSViewer implements Flushable {
/** /**
* @config * @config
* @var string The used "theme", which usually consists of templates, images and stylesheets. * @var string A list (highest priority first) of themes to use
* Only used when {@link $theme_enabled} is set to TRUE. * Only used when {@link $theme_enabled} is set to TRUE.
*/ */
private static $themes = [];
/**
* @deprecated 4.0..5.0
* @config
* @var string The used "theme", which usually consists of templates, images and stylesheets.
* Only used when {@link $theme_enabled} is set to TRUE, and $themes is empty
*/
private static $theme = null; private static $theme = null;
/** /**
@ -791,65 +809,33 @@ class SSViewer implements Flushable {
return $viewer; return $viewer;
} }
public static function set_themes($themes = []) {
Config::inst()->remove('SSViewer', 'themes');
Config::inst()->update('SSViewer', 'themes', $themes);
}
public static function add_themes($themes = []) {
Config::inst()->update('SSViewer', 'themes', $themes);
}
public static function get_themes() {
$res = ['$default'];
if (Config::inst()->get('SSViewer', 'theme_enabled')) {
if ($list = Config::inst()->get('SSViewer', 'themes')) $res = $list;
elseif ($theme = Config::inst()->get('SSViewer', 'theme')) $res = [$theme, '$default'];
}
return $res;
}
/** /**
* @deprecated 4.0 Use the "SSViewer.theme" config setting instead * @deprecated 4.0 Use the "SSViewer.theme" config setting instead
* @param string $theme The "base theme" name (without underscores). * @param string $theme The "base theme" name (without underscores).
*/ */
public static function set_theme($theme) { public static function set_theme($theme) {
Deprecation::notice('4.0', 'Use the "SSViewer.theme" config setting instead'); Deprecation::notice('4.0', 'Use the "SSViewer#set_themes" instead');
Config::inst()->update('SSViewer', 'theme', $theme); self::set_themes([$theme]);
}
/**
* @deprecated 4.0 Use the "SSViewer.theme" config setting instead
* @return string
*/
public static function current_theme() {
Deprecation::notice('4.0', 'Use the "SSViewer.theme" config setting instead');
return Config::inst()->get('SSViewer', 'theme');
}
/**
* Returns the path to the theme folder
*
* @return string
*/
public static function get_theme_folder() {
$theme = Config::inst()->get('SSViewer', 'theme');
return $theme ? THEMES_DIR . "/" . $theme : project();
}
/**
* Returns an array of theme names present in a directory.
*
* @param string $path
* @param bool $subthemes Include subthemes (default false).
* @return array
*/
public static function get_themes($path = null, $subthemes = false) {
$path = rtrim($path ? $path : THEMES_PATH, '/');
$themes = array();
if (!is_dir($path)) return $themes;
foreach (scandir($path) as $item) {
if ($item[0] != '.' && is_dir("$path/$item")) {
if ($subthemes || strpos($item, '_') === false) {
$themes[$item] = $item;
}
}
}
return $themes;
}
/**
* @deprecated since version 4.0
* @return string
*/
public static function current_custom_theme(){
Deprecation::notice('4.0', 'Use the "SSViewer.theme" and "SSViewer.theme_enabled" config settings instead');
return Config::inst()->get('SSViewer', 'theme_enabled') ? Config::inst()->get('SSViewer', 'theme') : null;
} }
/** /**
@ -873,6 +859,7 @@ class SSViewer implements Flushable {
foreach($classes as $class) { foreach($classes as $class) {
$template = $class . $suffix; $template = $class . $suffix;
if(SSViewer::hasTemplate($template)) $templates[] = $template; if(SSViewer::hasTemplate($template)) $templates[] = $template;
elseif(SSViewer::hasTemplate('Includes/'.$template)) $templates[] = 'Includes/'.$template;
// If the class is "Page_Controller", look for Page.ss // If the class is "Page_Controller", look for Page.ss
if(stripos($class,'_controller') !== false) { if(stripos($class,'_controller') !== false) {
@ -893,38 +880,34 @@ class SSViewer implements Flushable {
* array('MySpecificPage', 'MyPage', 'Page') * array('MySpecificPage', 'MyPage', 'Page')
* </code> * </code>
*/ */
public function __construct($templateList, TemplateParser $parser = null) { public function __construct($templates, TemplateParser $parser = null) {
if ($parser) { if ($parser) {
$this->setParser($parser); $this->setParser($parser);
} }
if(!is_array($templateList) && substr((string) $templateList,-3) == '.ss') { $this->setTemplate($templates);
$this->chosenTemplates['main'] = $templateList;
} else {
if(Config::inst()->get('SSViewer', 'theme_enabled')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = null;
}
$this->chosenTemplates = SS_TemplateLoader::instance()->findTemplates(
$templateList, $theme
);
}
if(!$this->chosenTemplates) { if(!$this->chosen) {
$templateList = (is_array($templateList)) ? $templateList : array($templateList); $message = 'None of the following templates could be found: ';
$message .= print_r($templates, true);
$message = 'None of the following templates could be found'; $themes = self::get_themes();
if(!$theme) { if(!$themes) {
$message .= ' (no theme in use)'; $message .= ' (no theme in use)';
} else { } else {
$message .= ' in theme "' . $theme . '"'; $message .= ' in themes "' . print_r($themes, true) . '"';
} }
user_error($message . ': ' . implode(".ss, ", $templateList) . ".ss", E_USER_WARNING); user_error($message, E_USER_WARNING);
} }
} }
public function setTemplate($templates) {
$this->templates = $templates;
$this->chosen = TemplateLoader::instance()->findTemplate($templates, self::get_themes());
$this->subTemplates = [];
}
/** /**
* Set the template parser that will be used in template generation * Set the template parser that will be used in template generation
* @param \TemplateParser $parser * @param \TemplateParser $parser
@ -954,19 +937,7 @@ class SSViewer implements Flushable {
* @return boolean * @return boolean
*/ */
public static function hasTemplate($templates) { public static function hasTemplate($templates) {
$manifest = SS_TemplateLoader::instance()->getManifest(); return (bool)TemplateLoader::instance()->findTemplate($templates, self::get_themes());
if(Config::inst()->get('SSViewer', 'theme_enabled')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = null;
}
foreach ((array) $templates as $template) {
if ($manifest->getCandidateTemplate($template, $theme)) return true;
}
return false;
} }
/** /**
@ -1033,7 +1004,7 @@ class SSViewer implements Flushable {
} }
public function exists() { public function exists() {
return $this->chosenTemplates; return $this->chosen;
} }
/** /**
@ -1043,21 +1014,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) {
$loader = SS_TemplateLoader::instance(); return TemplateLoader::instance()->findTemplate(['type' => $type, $identifier], self::get_themes());
if(Config::inst()->get('SSViewer', 'theme_enabled')) {
$theme = Config::inst()->get('SSViewer', 'theme');
} else {
$theme = null;
}
$found = $loader->findTemplates("$type/$identifier", $theme);
if (isset($found['main'])) {
return $found['main'];
}
else if (!empty($found)) {
$founds = array_values($found);
return $founds[0];
}
} }
/** /**
@ -1192,13 +1149,7 @@ class SSViewer implements Flushable {
public function process($item, $arguments = null, $inheritedScope = null) { public function process($item, $arguments = null, $inheritedScope = null) {
SSViewer::$topLevel[] = $item; SSViewer::$topLevel[] = $item;
if(isset($this->chosenTemplates['main'])) { $template = $this->chosen;
$template = $this->chosenTemplates['main'];
} else {
$keys = array_keys($this->chosenTemplates);
$key = reset($keys);
$template = $this->chosenTemplates[$key];
}
$cacheFile = TEMP_FOLDER . "/.cache" $cacheFile = TEMP_FOLDER . "/.cache"
. str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template))); . str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
@ -1218,14 +1169,27 @@ class SSViewer implements Flushable {
// Makes the rendered sub-templates available on the parent item, // Makes the rendered sub-templates available on the parent item,
// through $Content and $Layout placeholders. // through $Content and $Layout placeholders.
foreach(array('Content', 'Layout') as $subtemplate) { foreach(array('Content', 'Layout') as $subtemplate) {
if(isset($this->chosenTemplates[$subtemplate])) { $sub = null;
if(isset($this->subTemplates[$subtemplate])) {
$sub = $this->subTemplates[$subtemplate];
}
elseif(!is_array($this->templates)) {
$sub = ['type' => $subtemplate, $this->templates];
}
elseif(!array_key_exists('type', $this->templates) || !$this->templates['type']) {
$sub = array_merge($this->templates, ['type' => $subtemplate]);
}
if ($sub) {
$subtemplateViewer = clone $this; $subtemplateViewer = clone $this;
// Disable requirements - this will be handled by the parent template // Disable requirements - this will be handled by the parent template
$subtemplateViewer->includeRequirements(false); $subtemplateViewer->includeRequirements(false);
// The subtemplate is the only file we want to process, so set it as the "main" template file // Select the right template
$subtemplateViewer->chosenTemplates = array('main' => $this->chosenTemplates[$subtemplate]); $subtemplateViewer->setTemplate($sub);
$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments); if ($subtemplateViewer->exists()) {
$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments);
}
} }
} }
@ -1301,7 +1265,7 @@ class SSViewer implements Flushable {
* 'Content' & 'Layout', and will have to contain 'main' * 'Content' & 'Layout', and will have to contain 'main'
*/ */
public function templates() { public function templates() {
return $this->chosenTemplates; return array_merge(['main' => $this->chosen], $this->subTemplates);
} }
/** /**
@ -1309,7 +1273,8 @@ class SSViewer implements Flushable {
* @param string $file Full system path to the template file * @param string $file Full system path to the template file
*/ */
public function setTemplateFile($type, $file) { public function setTemplateFile($type, $file) {
$this->chosenTemplates[$type] = $file; if (!$type || $type == 'main') $this->chosen = $file;
else $this->subTemplates[$type] = $file;
} }
/** /**

118
view/TemplateLoader.php Normal file
View File

@ -0,0 +1,118 @@
<?php
namespace SilverStripe\View;
/**
* Handles finding templates from a stack of template manifest objects.
*
* @package framework
* @subpackage view
*/
class TemplateLoader {
/**
* @var TemplateLoader
*/
private static $instance;
protected $base;
protected $sets = [];
public static function instance() {
return self::$instance ? self::$instance : self::$instance = new self();
}
public static function set_instance(TemplateLoader $instance) {
self::$instance = $instance;
}
public function __construct($base = null) {
$this->base = $base ? $base : BASE_PATH;
}
public function addSet($set, $manifest) {
$this->sets[$set] = $manifest;
}
public function getPath($identifier) {
$slashPos = strpos($identifier, '/');
// If identifier starts with "/", it's a path from root
if ($slashPos === 0) {
return substr($identifier, 1);
}
// Otherwise if there is a "/", identifier is a vendor'ed module
elseif ($slashPos !== false) {
$parts = explode(':', $identifier, 2);
list($vendor, $module) = explode('/', $parts[0], 2);
$theme = count($parts) > 1 ? $parts[1] : '';
$path = $module . ($theme ? '/themes/'.$theme : '');
// Right now we require $module to be a silverstripe module (in root) or theme (in themes dir)
// If both exist, we prefer theme
if (is_dir(THEMES_PATH . '/' .$path)) {
return THEMES_DIR . '/' . $path;
}
else {
return $path;
}
}
// Otherwise it's a (deprecated) old-style "theme" identifier
else {
return THEMES_DIR.'/'.$identifier;
}
}
/**
* Attempts to find possible candidate templates from a set of template
* names from modules, current theme directory and finally the application
* folder.
*
* The template names can be passed in as plain strings, or be in the
* format "type/name", where type is the type of template to search for
* (e.g. Includes, Layout).
*
* @param string|array $templates
* @param string $theme
*
* @return array
*/
public function findTemplate($template, $themes = []) {
if(is_array($template)) {
$type = array_key_exists('type', $template) ? $template['type'] : '';
$templateList = array_key_exists('templates', $template) ? $template['templates'] : $template;
}
else {
$type = '';
$templateList = array($template);
}
if(count($templateList) == 1 && substr($templateList[0], -3) == '.ss') {
return $templateList[0];
}
foreach($templateList as $i => $template) {
$template = str_replace('\\', '/', $template);
$parts = explode('/', $template);
$tail = array_pop($parts);
$head = implode('/', $parts);
foreach($themes as $themename) {
$subthemes = isset($this->sets[$themename]) ? $this->sets[$themename]->getThemes() : [$themename];
foreach($subthemes as $theme) {
$themePath = $this->base . '/' . $this->getPath($theme);
$path = $themePath . '/templates/' . implode('/', array_filter([$head, $type, $tail])) . '.ss';
if (file_exists($path)) return $path;
}
}
}
}
}

140
view/ThemeManifest.php Normal file
View File

@ -0,0 +1,140 @@
<?php
namespace SilverStripe\View;
use \ManifestFileFinder;
/**
* A class which builds a manifest of all themes (which is really just a directory called "templates")
*
* @package framework
* @subpackage manifest
*/
class ThemeManifest {
const TEMPLATES_DIR = 'templates';
protected $base;
protected $tests;
protected $project;
protected $cache;
protected $cacheKey;
protected $themes = null;
/**
* Constructs a new template manifest. The manifest is not actually built
* or loaded from cache until needed.
*
* @param string $base The base path.
* @param string $project Path to application code
*
* @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function __construct($base, $project, $includeTests = false, $forceRegen = false) {
$this->base = $base;
$this->tests = $includeTests;
$this->project = $project;
$cacheClass = defined('SS_MANIFESTCACHE') ? SS_MANIFESTCACHE : 'ManifestCache_File';
$this->cache = new $cacheClass('thememanifest'.($includeTests ? '_tests' : ''));
$this->cacheKey = $this->getCacheKey();
if ($forceRegen) {
$this->regenerate();
}
}
/**
* @return string
*/
public function getBase() {
return $this->base;
}
/**
* Generate a unique cache key to avoid manifest cache collisions.
* We compartmentalise based on the base path, the given project, and whether
* or not we intend to include tests.
* @return string
*/
public function getCacheKey() {
return sha1(sprintf(
"manifest-%s-%s-%u",
$this->base,
$this->project,
$this->tests
));
}
/**
* 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() {
if ($this->themes === null) $this->init();
return $this->themes;
}
/**
* Regenerates the manifest by scanning the base path.
*
* @param bool $cache
*/
public function regenerate($cache = true) {
$finder = new ManifestFileFinder();
$finder->setOptions(array(
'include_themes' => false,
'ignore_tests' => !$this->tests,
'dir_callback' => array($this, 'handleDirectory')
));
$this->themes = [];
$finder->find($this->base);
if ($cache) {
$this->cache->save($this->themes, $this->cacheKey);
}
}
public function handleDirectory($basename, $pathname, $depth)
{
if ($basename == self::TEMPLATES_DIR) {
// We only want part of the full path, so split into directories
$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.
if ($themeParts[0] == $this->project) {
array_unshift($this->themes, $path);
}
else {
array_push($this->themes, $path);
}
}
}
protected function init() {
if ($data = $this->cache->load($this->cacheKey)) {
$this->themes = $data;
} else {
$this->regenerate();
}
}
}