silverstripe-docsviewer/code/controllers/DocumentationViewer.php
Uncle Cheese c71a1aaca2 These changes make a number of improvements, mostly typographical, to improve readability and to render properly on small devices.
Universal increase in font size
Use same font as ss.org for consistency
Remove unnecessary visual noise (borders, box shadows, etc)
Use Google PrettyPrint syntax highlighter for improved readability of code sections
Fully responsive, with hamburger menu
Add "section" linking mode to menu items
Add masthead for breadcrumbs, introduction
Improved styles for notice/info/warning messaging
2014-11-07 12:01:07 +13:00

654 lines
14 KiB
PHP
Executable File

<?php
/**
* Documentation Viewer.
*
* Reads the bundled markdown files from documentation folders and displays the
* output (either via markdown or plain text).
*
* For more documentation on how to use this class see the documentation in the
* docs folder.
*
* @package docsviewer
*/
class DocumentationViewer extends Controller {
/**
* @var array
*/
private static $extensions = array(
'DocumentationViewerVersionWarning',
'DocumentationSearchExtension'
);
/**
* @var string
*/
private static $google_analytics_code = '';
/**
* @var string
*/
private static $documentation_title = 'SilverStripe Documentation';
/**
* @var array
*/
private static $allowed_actions = array(
'all',
'results',
'handleAction'
);
/**
* The string name of the currently accessed {@link DocumentationEntity}
* object. To access the entire object use {@link getEntity()}
*
* @var string
*/
protected $entity = '';
/**
* @var DocumentationPage
*/
protected $record;
/**
* @var DocumentationManifest
*/
protected $manifest;
/**
* @config
*
* @var string same as the routing pattern set through Director::addRules().
*/
private static $link_base = 'dev/docs/';
/**
* @config
*
* @var string|array Optional permission check
*/
private static $check_permission = 'ADMIN';
/**
* @var array map of modules to edit links.
* @see {@link getEditLink()}
*/
private static $edit_links = array();
/**
*
*/
public function init() {
parent::init();
if(!$this->canView()) {
return Security::permissionFailure($this);
}
Requirements::javascript('//use.typekit.net/emt4dhq.js');
Requirements::customScript('try{Typekit.load();}catch(e){}');
Requirements::javascript(THIRDPARTY_DIR .'/jquery/jquery.js');
Requirements::javascript('https://google-code-prettify.googlecode.com/svn/loader/run_prettify.js');
Requirements::javascript(DOCSVIEWER_DIR .'/javascript/DocumentationViewer.js');
Requirements::combine_files('docs.css', array(
DOCSVIEWER_DIR .'/css/normalize.css',
DOCSVIEWER_DIR .'/css/utilities.css',
DOCSVIEWER_DIR .'/css/typography.css',
DOCSVIEWER_DIR .'/css/forms.css',
DOCSVIEWER_DIR .'/css/layout.css',
DOCSVIEWER_DIR .'/css/small.css'
));
}
/**
* Can the user view this documentation. Hides all functionality for private
* wikis.
*
* @return bool
*/
public function canView() {
return (Director::isDev() || Director::is_cli() ||
!$this->config()->get('check_permission') ||
Permission::check($this->config()->get('check_permission'))
);
}
public function hasAction($action) {
return true;
}
public function checkAccessAction($action) {
return true;
}
/**
* Overloaded to avoid "action doesn't exist" errors - all URL parts in
* this controller are virtual and handled through handleRequest(), not
* controller methods.
*
* @param $request
* @param $action
*
* @return SS_HTTPResponse
*/
public function handleAction($request, $action) {
// if we submitted a form, let that pass
if(!$request->isGET()) {
return parent::handleAction($request, $action);
}
$url = $request->getURL();
//
// If the current request has an extension attached to it, strip that
// off and redirect the user to the page without an extension.
//
if(DocumentationHelper::get_extension($url)) {
$this->response = new SS_HTTPResponse();
$this->response->redirect(
DocumentationHelper::trim_extension_off($url) .'/',
301
);
$request->shift();
$request->shift();
return $this->response;
}
//
// Strip off the base url
//
$base = ltrim(
Config::inst()->get('DocumentationViewer', 'link_base'), '/'
);
if($base && strpos($url, $base) !== false) {
$url = substr(
ltrim($url, '/'),
strlen($base)
);
} else {
}
//
// Handle any permanent redirections that the developer has defined.
//
if($link = DocumentationPermalinks::map($url)) {
// the first param is a shortcode for a page so redirect the user to
// the short code.
$this->response = new SS_HTTPResponse();
$this->response->redirect($link, 301);
$request->shift();
$request->shift();
return $this->response;
}
//
// Validate the language provided. Language is a required URL parameter.
// as we use it for generic interfaces and language selection. If
// language is not set, redirects to 'en'
//
$languages = i18n::get_common_languages();
if(!$lang = $request->param('Lang')) {
$lang = $request->param('Action');
$action = $request->param('ID');
} else {
$action = $request->param('Action');
}
if(!$lang) {
return $this->redirect($this->Link('en'));
} else if(!isset($languages[$lang])) {
return $this->httpError(404);
}
$request->shift(10);
$allowed = $this->config()->allowed_actions;
if(in_array($action, $allowed)) {
//
// if it's one of the allowed actions such as search or all then the
// URL must be prefixed with one of the allowed languages.
//
return parent::handleAction($request, $action);
} else {
//
// look up the manifest to see find the nearest match against the
// list of the URL. If the URL exists then set that as the current
// page to match against.
// strip off any extensions.
// if($cleaned !== $url) {
// $redirect = new SS_HTTPResponse();
// return $redirect->redirect($cleaned, 302);
// }
if($record = $this->getManifest()->getPage($url)) {
$this->record = $record;
$this->init();
$type = get_class($this->record);
$body = $this->renderWith(array(
"DocumentationViewer_{$type}",
"DocumentationViewer"
));
return new SS_HTTPResponse($body, 200);
} else if(!$url || $url == $lang) {
$body = $this->renderWith(array(
"DocumentationViewer_DocumentationFolder",
"DocumentationViewer"
));
return new SS_HTTPResponse($body, 200);
}
}
return $this->httpError(404);
}
/**
* @param int $status
* @param string $message
*
* @return SS_HTTPResponse
*/
public function httpError($status, $message = null) {
$this->init();
$class = get_class($this);
$body = $this->customise(new ArrayData(array(
'Message' => $message
)))->renderWith(array("{$class}_error", $class));
return new SS_HTTPResponse($body, $status);
}
/**
* @return DocumentationManifest
*/
public function getManifest() {
if(!$this->manifest) {
$flush = SapphireTest::is_running_test() || (isset($_GET['flush']));
$this->manifest = new DocumentationManifest($flush);
}
return $this->manifest;
}
/**
* @return string
*/
public function getLanguage() {
if(!$lang = $this->request->param('Lang')) {
$lang = $this->request->param('Action');
}
return $lang;
}
/**
* Generate a list of {@link Documentation } which have been registered and which can
* be documented.
*
* @return DataObject
*/
public function getMenu() {
$entities = $this->getManifest()->getEntities();
$output = new ArrayList();
$record = $this->getPage();
$current = $this->getEntity();
foreach($entities as $entity) {
// only show entities with the same language
if($entity->getLanguage() !== $this->getLanguage()) {
continue;
}
$mode = 'link';
$children = new ArrayList();
if($entity->hasRecord($record) || $entity->getIsDefaultEntity()) {
$mode = 'current';
// add children
$children = $this->getManifest()->getChildrenFor(
$entity->getPath(), ($record) ? $record->getPath() : $entity->getPath()
);
} else {
if($current && $current->getKey() == $entity->getKey()) {
continue;
}
}
$link = $entity->Link();
$output->push(new ArrayData(array(
'Title' => $entity->getTitle(),
'Link' => $link,
'LinkingMode' => $mode,
'DefaultEntity' => $entity->getIsDefaultEntity(),
'Children' => $children
)));
}
return $output;
}
/**
* Return the content for the page. If its an actual documentation page then
* display the content from the page, otherwise display the contents from
* the index.md file if its a folder
*
* @return HTMLText
*/
public function getContent() {
$page = $this->getPage();
$html = $page->getHTML();
$html = $this->replaceChildrenCalls($html);
return $html;
}
public function replaceChildrenCalls($html) {
$codes = new ShortcodeParser();
$codes->register('CHILDREN', array($this, 'includeChildren'));
return $codes->parse($html);
}
/**
* Short code parser
*/
public function includeChildren($args) {
if(isset($args['Folder'])) {
$children = $this->getManifest()->getChildrenFor(
Controller::join_links(dirname($this->record->getPath()), $args['Folder'])
);
} else {
$children = $this->getManifest()->getChildrenFor(
dirname($this->record->getPath())
);
}
if(isset($args['Exclude'])) {
$exclude = explode(',', $args['Exclude']);
foreach($children as $k => $child) {
foreach($exclude as $e) {
if($child->Link == Controller::join_links($this->record->Link(), strtolower($e), '/')) {
unset($children[$k]);
}
}
}
}
return $this->customise(new ArrayData(array(
'Children' => $children
)))->renderWith('Includes/DocumentationPages');
}
/**
* @return ArrayList
*/
public function getChildren() {
if($this->record instanceof DocumentationFolder) {
return $this->getManifest()->getChildrenFor(
$this->record->getPath()
);
} else if($this->record) {
return $this->getManifest()->getChildrenFor(
dirname($this->record->getPath())
);
}
return new ArrayList();
}
/**
* Generate a list of breadcrumbs for the user.
*
* @return ArrayList
*/
public function getBreadcrumbs() {
if($this->record) {
return $this->getManifest()->generateBreadcrumbs(
$this->record,
$this->record->getEntity()
);
}
}
/**
* @return DocumentationPage
*/
public function getPage() {
return $this->record;
}
/**
* @return DocumentationEntity
*/
public function getEntity() {
return ($this->record) ? $this->record->getEntity() : null;
}
/**
* @return ArrayList
*/
public function getVersions() {
return $this->manifest->getVersions($this->getEntity);
}
/**
* Generate a string for the title tag in the URL.
*
* @return string
*/
public function getTitle() {
return ($this->record) ? $this->record->getTitle() : null;
}
/**
* @return string
*/
public function AbsoluteLink($action) {
return Controller::join_links(
Director::absoluteBaseUrl(),
$this->Link($action)
);
}
/**
* Return the base link to this documentation location.
*
* @return string
*/
public function Link($action = '') {
$link = Controller::join_links(
Config::inst()->get('DocumentationViewer', 'link_base'),
$this->getLanguage(),
$action,
'/'
);
return $link;
}
/**
* Generate a list of all the pages in the documentation grouped by the
* first letter of the page.
*
* @return GroupedList
*/
public function AllPages() {
$pages = $this->getManifest()->getPages();
$output = new ArrayList();
foreach($pages as $url => $page) {
$first = strtoupper(trim(substr($page['title'], 0, 1)));
if($first) {
$output->push(new ArrayData(array(
'Link' => $url,
'Title' => $page['title'],
'FirstLetter' => $first
)));
}
}
return GroupedList::create($output->sort('Title', 'ASC'));
}
/**
* Documentation Search Form. Allows filtering of the results by many entities
* and multiple versions.
*
* @return Form
*/
public function DocumentationSearchForm() {
if(!Config::inst()->get('DocumentationSearch','enabled')) {
return false;
}
return new DocumentationSearchForm($this);
}
/**
* Sets the mapping between a entity name and the link for the end user
* to jump into editing the documentation.
*
* Some variables are replaced:
* - %version%
* - %entity%
* - %path%
* - %lang%
*
* For example to provide an edit link to the framework module in github:
*
* <code>
* DocumentationViewer::set_edit_link(
* 'framework',
* 'https://github.com/silverstripe/%entity%/edit/%version%/docs/%lang%/%path%',
* $opts
* ));
* </code>
*
* @param string module name
* @param string link
* @param array options ('rewritetrunktomaster')
*/
public static function set_edit_link($module, $link, $options = array()) {
self::$edit_links[$module] = array(
'url' => $link,
'options' => $options
);
}
/**
* Returns an edit link to the current page (optional).
*
* @return string
*/
public function getEditLink() {
$page = $this->getPage();
if($page) {
$entity = $page->getEntity();
if($entity && isset(self::$edit_links[$entity->title])) {
// build the edit link, using the version defined
$url = self::$edit_links[$entity->title];
$version = $this->getVersion();
if($version == "trunk" && (isset($url['options']['rewritetrunktomaster']))) {
if($url['options']['rewritetrunktomaster']) {
$version = "master";
}
}
return str_replace(
array('%entity%', '%lang%', '%version%', '%path%'),
array(
$entity->getBaseFolder(),
$this->getLanguage(),
$version,
ltrim($page->getPath(), '/')
),
$url['url']
);
}
}
return false;
}
/**
* Returns the next page. Either retrieves the sibling of the current page
* or return the next sibling of the parent page.
*
* @return DocumentationPage
*/
public function getNextPage() {
return ($this->record)
? $this->getManifest()->getNextPage(
$this->record->getPath(), $this->getEntity()->getPath())
: null;
}
/**
* Returns the previous page. Either returns the previous sibling or the
* parent of this page
*
* @return DocumentationPage
*/
public function getPreviousPage() {
return ($this->record)
? $this->getManifest()->getPreviousPage(
$this->record->getPath(), $this->getEntity()->getPath())
: null;
}
/**
* @return string
*/
public function getGoogleAnalyticsCode() {
$code = $this->config()->get('google_analytics_code');
if($code) {
return $code;
}
}
/**
* @return string
*/
public function getDocumentationTitle() {
return $this->config()->get('documentation_title');
}
public function getDocumentationBaseHref() {
return Config::inst()->get('DocumentationViewer', 'link_base');
}
}