Overhaul of module to use DocumentationManifest

This major update changes the behaviour of the docviewer module to use a cached manifest rather than on demand. This allows us to simplify the URL matching and store 'nice' URL configuration rather than altering handleAction().
This commit is contained in:
Will Rossiter 2014-09-07 11:26:12 +12:00
parent e80edc445e
commit 43b6d42719
41 changed files with 1398 additions and 1762 deletions

View File

@ -13,35 +13,39 @@
## Summary
Reads text files from a given list of folders from your installation and
provides a web interface for viewing.
Reads markdown files from a given list of folders from your installation and
provides a web interface for viewing the documentation. Ideal for providing
documentation alongside your module or project code.
To read documentation go to yoursite.com/dev/docs/
A variation of this module powers the main SilverStripe developer documentation
and the user help websites.
For more documentation on how to use the module please read /docs/Writing-Documentation.md
(or via this in /dev/docs/docsviewer/Writing-Documentation in your webbrowser)
**Note** This module assumes you are using numeric values for your versions.
## Installation
### HTML Publishing
composer require "silverstripe/docsviewer" "dev-master"
If you wish to generate a truly static version of your documentation after it
has been rendered through the website, add the [Static Publisher](https://github.com/silverstripe-labs/silverstripe-staticpublisher)
module to your documentation project and set the following configuration in your
applications config.yml:
## Usage
```
StaticExporter:
extensions:
- DocumentationStaticPublisherExtension
```
After installing the files via composer, rebuild the SilverStripe database..
If you don't plan on using static publisher for anything else and you have the
cms module installed, make sure you disable the CMS from being published.
sake dev/build
Again, in your applications config.yml file
Then start by viewing the documentation at `yoursite.com/dev/docs`.
```
StaticExporter:
disable_sitetree_export: true
```
Out of the box the module will display the documentation files that have been
bundled into any of your installed modules. To configure what is shown in the
documentation viewer see the detailed [documentation](docs/en/configuration.md).
For more information about how to use the module see each of the documentation
* [Configuration](docs/en/configuration.md)
* [Markdown Syntax](docs/en/markdown.md)
* [Syntax Highlighting](docs/en/syntax-highlighting.md)
* [Publishing Static Files](docs/en/statichtml.md)
## License
See LICENSE

View File

@ -15,9 +15,3 @@ if(!defined('DOCSVIEWER_DIR')) {
define('DOCSVIEWER_DIR', array_pop($dir));
}
// define filetypes to ignore
DocumentationService::set_ignored_files(array(
'.', '..', '.DS_Store',
'.svn', '.git', 'assets', 'themes', '_images', '_resources'
));

View File

@ -40,4 +40,72 @@ class DocumentationHelper {
return $output;
}
/**
* String helper for cleaning a file name to a readable version.
*
* @param string $name to convert
*
* @return string $name output
*/
public static function clean_page_name($name) {
$name = self::trim_extension_off($name);
$name = self::trim_sort_number($name);
$name = str_replace(array('-', '_'), ' ', $name);
return ucwords(trim($name));
}
/**
* String helper for cleaning a file name to a URL safe version.
*
* @param string $name to convert
*
* @return string $name output
*/
public static function clean_page_url($name) {
$name = str_replace(array(' '), '_', $name);
$name = self::trim_extension_off($name);
$name = self::trim_sort_number($name);
if(preg_match('/^[\/]?index[\/]?/', $name)) {
return '';
}
return strtolower($name);
}
/**
* Removes leading numbers from pages (used to control sort order).
*
* @param string
*
* @return string
*/
public static function trim_sort_number($name) {
$name = preg_replace("/^[0-9]*[_-]+/", '', $name);
return $name;
}
/**
* Helper function to strip the extension off and return the name without
* the extension.
*
* @param string
*
* @return string
*/
public static function trim_extension_off($name) {
if(strrpos($name,'.') !== false) {
return substr($name, 0, strrpos($name,'.'));
}
return $name;
}
}

View File

@ -0,0 +1,267 @@
<?php
/**
* A class which builds a manifest of all documentation present in a project.
*
* The manifest is required to map the provided documentation URL rules to a
* file path on the server. The stored cache looks similar to the following:
*
* <code>
* array(
* 'en/someniceurl/' => array(
* 'filepath' => '/path/to/docs/en/SomeniceFile.md',
* 'title' => 'Some nice URL',
* 'summary' => 'Summary Text',
* 'basename' => 'SomeniceFile.md',
* 'type' => 'DocumentationPage'
* )
* )
* </code>
*
* URL format is in the following structures:
*
* {lang}/{path}
* {lang}/{module}/{path}
* {lang}/{module}/{version}/{/path}
*
* @package framework
* @subpackage manifest
*/
class DocumentationManifest {
const TEMPLATES_DIR = 'documentation';
protected $base;
protected $cache;
protected $cacheKey;
protected $inited;
protected $forceRegen;
protected $pages = array();
private $entity;
/**
* Constructs a new template manifest. The manifest is not actually built
* or loaded from cache until needed.
*
* @param bool $includeTests Include tests in the manifest.
* @param bool $forceRegen Force the manifest to be regenerated.
*/
public function __construct($forceRegen = false) {
$this->cacheKey = 'manifest';
$this->forceRegen = $forceRegen;
$this->cache = SS_Cache::factory('DocumentationManifest', 'Core', array(
'automatic_serialization' => true,
'lifetime' => null
));
}
/**
*
*/
protected function init() {
if (!$this->forceRegen && $data = $this->cache->load($this->cacheKey)) {
$this->pages = $data;
$this->inited = true;
} else {
$this->regenerate();
}
}
/**
* Returns a map of all documentation pages.
*
* @return array
*/
public function getPages() {
if (!$this->inited) {
$this->init();
}
return $this->pages;
}
/**
* Returns a particular page for the requested URL.
*
* @return DocumentationPage
*/
public function getPage($url) {
$pages = $this->getPages();
if(!isset($pages[$url])) {
return null;
}
$record = $pages[$url];
DocumentationService::load_automatic_registration();
foreach(DocumentationService::get_registered_entities() as $entity) {
foreach($entity->getVersions() as $version) {
foreach($version->getSupportedLanguages() as $language) {
if(strpos($record['filepath'], $language->getPath()) !== false) {
$page = Injector::inst()->create(
$record['type'],
$language,
$record['basename'],
$record['filepath']
);
return $page;
}
}
}
}
}
/**
* Regenerates the manifest by scanning the base path.
*
* @param bool $cache
*/
public function regenerate($cache = true) {
$finder = new DocumentationManifestFileFinder();
$finder->setOptions(array(
'dir_callback' => array($this, 'handleFolder'),
'file_callback' => array($this, 'handleFile')
));
DocumentationService::load_automatic_registration();
foreach(DocumentationService::get_registered_entities() as $entity) {
foreach($entity->getVersions() as $version) {
foreach($version->getSupportedLanguages() as $k => $v) {
$this->entity = $v;
$this->handleFolder('', $this->entity->getPath(), 0);
$finder->find($this->entity->getPath());
}
}
}
if ($cache) {
$this->cache->save($this->pages, $this->cacheKey);
}
$this->inited = true;
}
/**
*
*/
public function handleFolder($basename, $path, $depth) {
$folder = Injector::inst()->create(
'DocumentationFolder', $this->entity, $basename, $path
);
$this->pages[$folder->Link()] = array(
'title' => $folder->getTitle(),
'basename' => $basename,
'filepath' => $path,
'type' => 'DocumentationFolder'
);
}
/**
* Individual files can optionally provide a nice title and a better URL
* through the use of markdown meta data. This creates a new
* {@link DocumentationPage} instance for the file.
*
* If the markdown does not specify the title in the meta data it falls back
* to using the file name.
*
* @param string $basename
* @param string $path
* @param int $depth
*/
public function handleFile($basename, $path, $depth) {
$page = Injector::inst()->create(
'DocumentationPage',
$this->entity, $basename, $path
);
$this->pages[$page->Link()] = array(
'title' => $page->getTitle(),
'filepath' => $path,
'basename' => $basename,
'type' => 'DocumentationPage',
'summary' => $page->getSummary()
);
}
/**
* Generate an {@link ArrayList} of the pages to the given page.
*
* @return ArrayList
*/
public function generateBreadcrumbs($record) {
$output = new ArrayList();
// @todo
return $output;
}
/**
* Determine the next page from the given page.
*
* Relies on the fact when the manifest was built, it was generated in
* order.
*
* @param string
*
* @return ArrayData
*/
public function getNextPage($filepath) {
$grabNext = false;
foreach($this->getPages() as $url => $page) {
if($grabNext) {
return new ArrayData(array(
'Link' => $url,
'Title' => $page['title']
));
}
if($filepath == $page['filepath']) {
$grabNext = true;
}
}
return null;
}
/**
* Determine the previous page from the given page.
*
* Relies on the fact when the manifest was built, it was generated in
* order.
*
* @param string
*
* @return ArrayData
*/
public function getPreviousPage($filepath) {
$previousUrl = $previousPage = null;
foreach($this->getPages() as $url => $page) {
if($filepath == $page['filepath']) {
if($previousUrl) {
return new ArrayData(array(
'Link' => $previousUrl,
'Title' => $previousPage['title']
));
}
}
$previousUrl = $url;
$previousPage = $page;
}
return null;
}
}

View File

@ -0,0 +1,38 @@
<?php
class DocumentationManifestFileFinder extends SS_FileFinder {
/**
* @var array
*/
private static $ignored_files = array(
'.', '..', '.ds_store',
'.svn', '.git', 'assets', 'themes', '_images'
);
/**
* @var array
*/
protected static $default_options = array(
'name_regex' => '/\.(md|markdown)$/i',
'file_callback' => null,
'dir_callback' => null,
'ignore_vcs' => true
);
/**
*
*/
public function acceptDir($basename, $pathname, $depth) {
$ignored = Config::inst()->get('DocumentationManifestFileFinder', 'ignored_files');
if($ignored) {
if(in_array(strtolower($basename), $ignored)) {
return false;
}
}
return true;
}
}

View File

@ -348,7 +348,7 @@ class DocumentationParser {
// relative path (relative to module base folder), without the filename.
// For "sapphire/en/current/topics/templates", this would be "templates"
$relativePath = dirname($page->getRelativePath());
$relativePath = dirname($page->Link());
if($relativePath == '.') $relativePath = '';
// file base link

View File

@ -5,43 +5,14 @@
*
* Handles the management of the documentation services delivered by the entity.
*
* Includes registering which components to document and handles the entities being
* documented.
* Includes registering which components to document and handles the entities
* being documented.
*
* @package docsviewer
*/
class DocumentationService {
/**
* A mapping of known / popular languages to nice titles.
*
* @var Array
*/
private static $language_mapping = array(
'en' => 'English',
'fr' => 'Français',
'de' => 'Deutsch'
);
/**
* Files to ignore from any documentation listing.
*
* @var array
*/
private static $ignored_files = array(
'.', '..', '.DS_Store',
'.svn', '.git', 'assets', 'themes', '_images'
);
/**
* Case insenstive values to use as extensions on markdown pages. The
* recommended extension is .md.
*
* @var array
*/
public static $valid_markdown_extensions = array('md', 'txt', 'markdown');
/**
* Registered {@link DocumentationEntity} objects to include in the
* documentation.
@ -77,57 +48,6 @@ class DocumentationService {
*/
private static $automatic_registration = true;
/**
* by default pagenumbers start high at 10.000
*
* @var integer
*/
private static $pagenumber_start_at = 10000;
/**
* allow the use of key/value pairs in comments
*
* @var boolean
*/
private static $meta_comments_enabled = false;
/**
* Return the allowed extensions
*
* @return array
*/
public static function get_valid_extensions() {
return self::$valid_markdown_extensions;
}
/**
* Check to see if a given extension is a valid extension to be rendered.
* Assumes that $ext has a leading dot as that is what $valid_extension uses.
*
* @return bool
*/
public static function is_valid_extension($ext) {
return in_array(strtolower($ext), self::get_valid_extensions());
}
/**
* Set the ignored files list
*
* @param array
*/
public static function set_ignored_files($files) {
self::$ignored_files = $files;
}
/**
* Return the list of files which are ignored
*
* @return array
*/
public static function get_ignored_files() {
return self::$ignored_files;
}
/**
* Set automatic registration of entities and documentation folders
*
@ -138,8 +58,9 @@ class DocumentationService {
self::$automatic_registration = $bool;
if(!$bool) {
// remove current registed entities when disabling automatic registration
// needed to avoid caching issues when running all the tests
// remove current registed entities when disabling automatic
// registration needed to avoid caching issues when running all the
// tests
self::$registered_entities = array();
}
}
@ -152,43 +73,6 @@ class DocumentationService {
public static function automatic_registration_enabled() {
return self::$automatic_registration;
}
/**
* set the number to start default pagenumbering, allowing room for
* custom pagenumbers below.
*
* @param int $number
*/
public static function start_pagenumbers_at($number = 10000) {
if (is_int($number)) self::$pagenumber_start_at = $number;
}
/**
* return the startlevel for default pagenumbering
*
* @return int
*/
public static function get_pagenumber_start_at() {
return self::$pagenumber_start_at;
}
/**
* Allow the use of key/value pairs in comments?
*
* @param bool $allow
*/
public static function enable_meta_comments($allow = true) {
self::$meta_comments_enabled = (bool) $allow;
}
/**
* can we use key/value pairs
*
* @return bool
*/
public static function meta_comments_enabled() {
return self::$meta_comments_enabled;
}
/**
* Return the entities which are listed for documentation. Optionally only
@ -216,38 +100,13 @@ class DocumentationService {
}
/**
* Check to see if a entity is registered with the documenter.
* Register a entity to be included in the documentation. To unregister a
* entity use {@link DocumentationService::unregister()}.
*
* @param String $entity entity name
* @param String $version version
* @param String $lang language
*
* @return DocumentationEntity $entity the registered entity
*/
public static function is_registered_entity($entity, $version = false, $lang = false) {
$check = ($entity instanceof DocumentationEntity) ? $entity->getFolder() : (string) $entity;
if(isset(self::$registered_entities[$check])) {
$entity = self::$registered_entities[$check];
if(($lang && !$entity->hasLanguage($lang)) || ($version && !$entity->hasVersion($version))) {
return false;
}
return $entity;
}
return false;
}
/**
* Register a entity to be included in the documentation. To unregister a entity
* use {@link DocumentationService::unregister()}. Must include the trailing slash
*
* @param String $entity Name of entity to register
* @param String $path Path to documentation root.
* @param Float $version Version of entity.
* @param String $title Nice title to use
* @param string $entity Name of entity to register
* @param string $path Path to documentation root.
* @param float $version Version of entity.
* @param string $title Nice title to use
* @param bool $latest - return is this the latest release.
*
* @throws InvalidArgumentException
@ -255,38 +114,40 @@ class DocumentationService {
* @return DocumentationEntity
*/
public static function register($entity, $path, $version = '', $title = false, $latest = false) {
if(!file_exists($path)) throw new InvalidArgumentException(sprintf('Path "%s" doesn\'t exist', $path));
if(!file_exists($path)) {
throw new InvalidArgumentException(sprintf('Path "%s" doesn\'t exist', $path));
}
// add the entity to the registered array
if(!isset(self::$registered_entities[$entity])) {
// entity is completely new
$output = new DocumentationEntity($entity, $version, $path, $title);
self::$registered_entities[$entity] = $output;
$de = new DocumentationEntity($entity, $title);
self::$registered_entities[$entity] = $de;
}
else {
// entity exists so add the version to it
$output = self::$registered_entities[$entity];
$output->addVersion($version, $path);
$de = self::$registered_entities[$entity];
}
if($latest)
$output->setStableVersion($version);
return $output;
// create a new version of the entity and attach it the the entity
$dve = new DocumentationEntityVersion($de, $path, $version, $latest);
$de->addVersion($dve);
return $de;
}
/**
* Unregister a entity from being included in the documentation. Useful
* for keeping {@link DocumentationService::$automatic_registration} enabled
* but disabling entities which you do not want to show. Combined with a
* {@link Director::isLive()} you can hide entities you don't want a client to see.
* {@link Director::isLive()} you can hide entities you don't want a client
* to see.
*
* If no version or lang specified then the whole entity is removed. Otherwise only
* the specified version of the documentation.
* If no version or lang specified then the whole entity is removed.
* Otherwise only the specified version of the documentation.
*
* @param String $entity
* @param String $version
* @param string $entity
* @param string $version
*
* @return bool
*/
@ -296,9 +157,7 @@ class DocumentationService {
if($version) {
$entity->removeVersion($version);
}
else {
// only given a entity so unset the whole entity
} else {
unset(self::$registered_entities[$entityName]);
}
@ -315,292 +174,25 @@ class DocumentationService {
* @see {@link DocumentationService::set_automatic_registration()}
*/
public static function load_automatic_registration() {
if(self::automatic_registration_enabled()) {
$entities = scandir(BASE_PATH);
if(!self::automatic_registration_enabled()) {
return;
}
if($entities) {
foreach($entities as $key => $entity) {
$dir = is_dir(Controller::join_links(BASE_PATH, $entity));
$ignored = in_array($entity, self::get_ignored_files(), true);
if($dir && !$ignored) {
// check to see if it has docs
$docs = Director::baseFolder() . '/' . Controller::join_links($entity, 'docs');
if(is_dir($docs)) {
self::register($entity, $docs, 'current', $entity, true);
}
$entities = scandir(BASE_PATH);
if($entities) {
foreach($entities as $key => $entity) {
$dir = is_dir(Controller::join_links(BASE_PATH, $entity));
if($dir) {
// check to see if it has docs
$docs = Director::baseFolder() . '/' . Controller::join_links($entity, 'docs');
if(is_dir($docs)) {
self::register($entity, $docs, 'current', $entity, true);
}
}
}
}
}
/**
* Convert a language code to a 'nice' text string. Uses the
* {@link self::$language_mapping} array combined with translatable.
*
* @param String $code code
*/
public static function get_language_title($lang) {
$map = self::$language_mapping;
if(isset($map[$lang])) {
return _t("DOCUMENTATIONSERVICE.LANG-$lang", $map[$lang]);
}
return $lang;
}
/**
* Find a documentation page given a path and a file name. It ignores the
* extensions and simply compares the title.
*
* Name may also be a path /install/foo/bar.
*
* @param DocumentationEntity
* @param array exploded url string
* @param string version number
* @param string lang code
*
* @return String|false - File path
*/
static function find_page($entity, $path, $version = '', $lang = 'en') {
if($entity = self::is_registered_entity($entity, $version, $lang)) {
return self::find_page_recursive($entity->getPath($version, $lang), $path);
}
return false;
}
/**
* Recursive function for finding the goal of a path to a documentation
* page
*
* @return string
*/
private static function find_page_recursive($base, $goal) {
$handle = (is_dir($base)) ? opendir($base) : false;
$name = self::trim_extension_off(strtolower(array_shift($goal)));
if(!$name || $name == '/') $name = 'index';
if($handle) {
$ignored = self::get_ignored_files();
// ensure we end with a slash
$base = rtrim($base, '/') .'/';
while (false !== ($file = readdir($handle))) {
if(in_array($file, $ignored)) continue;
$formatted = self::trim_extension_off(strtolower($file));
// the folder is the one that we are looking for.
if(strtolower($name) == strtolower($formatted)) {
// if this file is a directory we could be displaying that
// or simply moving towards the goal.
if(is_dir(Controller::join_links($base, $file))) {
$base = $base . trim($file, '/') .'/';
// if this is a directory check that there is any more states to get
// to in the goal. If none then what we want is the 'index.md' file
if(count($goal) > 0) {
return self::find_page_recursive($base, $goal);
}
else {
// recurse but check for an index.md file next time around
return self::find_page_recursive($base, array('index'));
}
}
else {
// goal state. End of recursion.
// tidy up the URLs with single trailing slashes
$result = $base . ltrim($file, '/');
if(is_dir($result)) $result = (rtrim($result, '/') . '/');
return $result;
}
}
}
closedir($handle);
}
return false;
}
/**
* String helper for cleaning a file name to a readable version.
*
* @param String $name to convert
*
* @return String $name output
*/
public static function clean_page_name($name) {
// remove dashs and _
$name = str_replace(array('-', '_'), ' ', $name);
// remove extension
$name = self::trim_extension_off($name);
// if it starts with a number strip and contains a space strip it off
if(strpos($name, ' ') !== false) {
$space = strpos($name, ' ');
$short = substr($name, 0, $space);
if(is_numeric($short)) {
$name = substr($name, $space);
}
}
// convert first letter
return ucfirst(trim($name));
}
/**
* Helper function to strip the extension off and return the name without
* the extension. If you need the extension see {@link get_extension()}
*
* @param string
*
* @return string
*/
public static function trim_extension_off($name) {
$ext = self::get_extension($name);
if($ext) {
if(self::is_valid_extension($ext)) {
return substr($name, 0, strrpos($name,'.'));
}
}
return $name;
}
/**
* Returns the extension from a string. If you want to trim the extension
* off the end of the string see {@link trim_extension_off()}
*
* @param string
*
* @return string
*/
public static function get_extension($name) {
return substr(strrchr($name,'.'), 1);
}
/**
* Return the children from a given entity sorted by Title using natural ordering.
* It is used for building the tree of the page.
*
* @param DocumentationEntity path
* @param string - an optional path within a entity
* @param bool enable several recursive calls (more than 1 level)
* @param string - version to use
* @param string - lang to use
*
* @throws Exception
* @return ArrayList
*/
public static function get_pages_from_folder($entity, $relativePath = false, $recursive = true, $version = 'trunk', $lang = 'en') {
$output = new ArrayList();
$metaCommentsEnabled = self::meta_comments_enabled();
$pages = array();
if(!$entity instanceof DocumentationEntity)
user_error("get_pages_from_folder must be passed a entity", E_USER_ERROR);
$path = $entity->getPath($version, $lang);
if(self::is_registered_entity($entity)) {
self::get_pages_from_folder_recursive($path, $relativePath, $recursive, $pages);
}
else {
return user_error("$entity is not registered", E_USER_WARNING);
}
if(count($pages) > 0) {
$pagenumber = self::get_pagenumber_start_at();
natsort($pages);
foreach($pages as $key => $pagePath) {
// get file name from the path
$file = ($pos = strrpos($pagePath, '/')) ? substr($pagePath, $pos + 1) : $pagePath;
$page = new DocumentationPage();
$page->setTitle(self::clean_page_name($file));
$relative = str_replace($path, '', $pagePath);
// if no extension, put a slash on it
if(strpos($relative, '.') === false) $relative .= '/';
$page->setEntity($entity);
$page->setRelativePath($relative);
$page->setVersion($version);
$page->setLang($lang);
// does this page act as a folder?
$path = $page->getPath();
if (is_dir($path)) { $page->setIsFolder(true); }
$page->setPagenumber($pagenumber++);
// we need the markdown to get the comments
if ($metaCommentsEnabled) $page->getMarkdown();
$output->push($page);
}
}
return ($metaCommentsEnabled)? $output->sort('pagenumber') : $output;
}
/**
* Recursively search through a given folder
*
* @see {@link DocumentationService::get_pages_from_folder}
*/
private static function get_pages_from_folder_recursive($base, $relative, $recusive, &$pages) {
if(!is_dir($base)) throw new Exception(sprintf('%s is not a folder', $folder));
$folder = Controller::join_links($base, $relative);
if(!is_dir($folder)) return false;
$handle = opendir($folder);
if($handle) {
$ignore = self::get_ignored_files();
$files = array();
while (false !== ($file = readdir($handle))) {
if(!in_array($file, $ignore)) {
$path = Controller::join_links($folder, $file);
$relativeFilePath = Controller::join_links($relative, $file);
if(is_dir($path)) {
// dir
$pages[] = $relativeFilePath;
if($recusive) self::get_pages_from_folder_recursive($base, $relativeFilePath, $recusive, $pages);
}
else if(self::is_valid_extension(self::get_extension($path))) {
// file we want
$pages[] = $relativeFilePath;
}
}
}
}
closedir($handle);
}
}

View File

@ -0,0 +1,109 @@
<?php
class DocumentationSearchController extends DocumentationViewer {
/**
* Return an array of folders and titles
*
* @return array
*/
public function getSearchedEntities() {
$entities = array();
if(!empty($_REQUEST['Entities'])) {
if(is_array($_REQUEST['Entities'])) {
$entities = Convert::raw2att($_REQUEST['Entities']);
}
else {
$entities = explode(',', Convert::raw2att($_REQUEST['Entities']));
$entities = array_combine($entities, $entities);
}
}
else if($entity = $this->getEntity()) {
$entities[$entity->getFolder()] = Convert::raw2att($entity->getTitle());
}
return $entities;
}
/**
* Return an array of versions that we're allowed to return
*
* @return array
*/
public function getSearchedVersions() {
$versions = array();
if(!empty($_REQUEST['Versions'])) {
if(is_array($_REQUEST['Versions'])) {
$versions = Convert::raw2att($_REQUEST['Versions']);
$versions = array_combine($versions, $versions);
}
else {
$version = Convert::raw2att($_REQUEST['Versions']);
$versions[$version] = $version;
}
}
else if($version = $this->getVersion()) {
$version = Convert::raw2att($version);
$versions[$version] = $version;
}
return $versions;
}
/**
* Return the current search query
*
* @return HTMLText|null
*/
public function getSearchQuery() {
if(isset($_REQUEST['Search'])) {
return DBField::create_field('HTMLText', $_REQUEST['Search']);
}
}
/**
* Past straight to results, display and encode the query
*/
public function results($data, $form = false) {
$query = (isset($_REQUEST['Search'])) ? $_REQUEST['Search'] : false;
$search = new DocumentationSearch();
$search->setQuery($query);
$search->setVersions($this->getSearchedVersions());
$search->setModules($this->getSearchedEntities());
$search->setOutputController($this);
return $search->renderResults();
}
/**
* Returns an search form which allows people to express more complex rules
* and options than the plain search form.
*
* @todo client side filtering of checkable option based on the module selected.
*
* @return Form
*/
public function AdvancedSearchForm() {
$entities = DocumentationService::get_registered_entities();
return new DocumentationAdvancedSearchForm($this);
}
/**
* Check if the Advanced SearchForm can be displayed. It is enabled by
* default, to disable use:
*
* <code>
* DocumentationSearch::enable_advanced_search(false);
* </code>
*
* @return bool
*/
public function getAdvancedSearchEnabled() {
return DocumentationSearch::advanced_search_enabled();
}
}

View File

@ -35,16 +35,6 @@ class DocumentationViewer extends Controller {
* @var string
*/
private static $documentation_title = 'SilverStripe Documentation';
/**
* @var string
*/
public $version = "";
/**
* @var string
*/
public $language = "en";
/**
* The string name of the currently accessed {@link DocumentationEntity}
@ -52,27 +42,26 @@ class DocumentationViewer extends Controller {
*
* @var string
*/
public $entity = '';
/**
* @var array
*/
public $remaining = array();
protected $entity = '';
/**
* @var DocumentationPage
*/
public $currentLevelOnePage;
protected $record;
/**
* @var String Same as the routing pattern set through Director::addRules().
* @config
*
* @var string same as the routing pattern set through Director::addRules().
*/
protected static $link_base = 'dev/docs/';
private static $link_base = 'dev/docs/';
/**
* @var String|array Optional permission check
* @config
*
* @var string|array Optional permission check
*/
static $check_permission = 'ADMIN';
private static $check_permission = 'ADMIN';
/**
* @var array map of modules to edit links.
@ -80,6 +69,13 @@ class DocumentationViewer extends Controller {
*/
private static $edit_links = array();
/**
* @var array
*/
private static $url_handlers = array(
'$Action' => 'handleAction'
);
/**
*
*/
@ -162,217 +158,84 @@ class DocumentationViewer extends Controller {
* Handle the url parsing for the documentation. In order to make this
* user friendly this does some tricky things..
*
* The urls which should work
* / - index page
* /en/sapphire - the index page of sapphire (shows versions)
* /2.4/en/sapphire - the docs for 2.4 sapphire.
* /2.4/en/sapphire/installation/
*
* @return SS_HTTPResponse
*/
public function handleRequest(SS_HTTPRequest $request, DataModel $model) {
DocumentationService::load_automatic_registration();
$response = parent::handleRequest($request, $model);
// if we submitted a form, let that pass
if(!$request->isGET() || isset($_GET['action_results'])) {
return parent::handleRequest($request, $model);
}
$firstParam = ($request->param('Action')) ? $request->param('Action') : $request->shift();
$secondParam = $request->shift();
$thirdParam = $request->shift();
$this->Remaining = $request->shift(10);
// if no params passed at all then it's the homepage
if(!$firstParam && !$secondParam && !$thirdParam) {
return parent::handleRequest($request, $model);
return $response;
}
if($firstParam) {
// allow assets
if($firstParam == "assets") {
return parent::handleRequest($request, $model);
}
// check for permalinks
if($link = DocumentationPermalinks::map($firstParam)) {
// the first param is a shortcode for a page so redirect the user to
// the short code.
$this->response = new SS_HTTPResponse();
$this->redirect($link, 301); // 301 permanent redirect
return $this->response;
}
// 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.
if($record = $this->getManifest()->getPage($this->request->getURL())) {
$this->record = $record;
// check to see if the request is a valid entity. If it isn't, then we
// need to throw a 404.
if(!DocumentationService::is_registered_entity($firstParam)) {
return $this->throw404();
}
$this->entity = $firstParam;
$this->language = $secondParam;
if(isset($thirdParam) && (is_numeric($thirdParam) || in_array($thirdParam, array('master', 'trunk')))) {
$this->version = $thirdParam;
}
else {
// current version so store one area para
array_unshift($this->Remaining, $thirdParam);
$this->version = false;
}
}
// 'current' version mapping
$entity = DocumentationService::is_registered_entity($this->entity, null, $this->getLang());
$type = get_class($this->record);
$body = $this->renderWith(array(
"DocumentationViewer_{$type}",
"DocumentationViewer"
));
if($entity) {
$current = $entity->getStableVersion();
$version = $this->getVersion();
if(!$version) {
$this->version = $current;
}
// Check if page exists, otherwise return 404
if(!$this->locationExists()) {
return $this->throw404();
}
return parent::handleRequest($request, $model);
}
return $this->throw404();
}
/**
* Helper function for throwing a 404 error from the {@link handleRequest}
* method.
*
* @return HttpResponse
*/
function throw404() {
$this->init();
$class = get_class($this);
$body = $this->renderWith(array("{$class}_error", $class));
$this->response = new SS_HTTPResponse($body, 404);
return $this->response;
}
/**
* Custom templates for each of the sections.
*/
function getViewer($action) {
// count the number of parameters after the language, version are taken
// into account. This automatically includes ' ' so all the counts
// are 1 more than what you would expect
if($this->entity || $this->Remaining) {
$paramCount = count($this->Remaining);
if($paramCount == 0) {
return parent::getViewer('folder');
}
else if($entity = $this->getEntity()) {
// if this is a folder return the folder listing
if($this->locationExists() == 2) {
return parent::getViewer('folder');
}
}
return new SS_HTTPResponse($body, 200);
}
else {
return parent::getViewer('home');
}
$this->init();
return parent::getViewer($action);
$class = get_class($this);
$body = $this->renderWith(array("{$class}_error", $class));
return new SS_HTTPResponse($body, 404);
}
}
/**
* Returns the current version. If no version is set then it is the current
* set version so need to pull that from the {@link Entity}.
*
* @return String
* @return string
*/
function getVersion() {
if($this->version) return $this->version;
if($entity = $this->getEntity()) {
$this->version = $entity->getStableVersion();
return $this->version;
}
return false;
public function getVersion() {
return ($this->record) ? $this->record->getEntity()->getVersion() : null;
}
/**
* Returns the current language
* Returns the current language.
*
* @return String
* @return string
*/
function getLang() {
return $this->language;
public function getLanguage() {
return ($this->record) ? $this->record->getEntity()->getLanguage() : null;
}
/**
*
*/
public function getManifest() {
return new DocumentationManifest((isset($_GET['flush'])));
}
/**
* Return all the available languages for the {@link Entity}.
*
* @return array
*/
function getLanguages() {
$entity = $this->getEntity();
if($entity) {
return $entity->getLanguages();
}
return array('en' => 'English');
public function getLanguages() {
return ($this->record) ? $this->record->getEntity()->getSupportedLanguages() : null;
}
/**
* Get all the versions loaded for the current {@link DocumentationEntity}.
* the filesystem then they are loaded under the 'Current' namespace.
* the file system then they are loaded under the 'Current' name space.
*
* @param String $entity name of {@link Entity} to limit it to eg sapphire
* @return ArrayList
*/
function getVersions($entity = false) {
if(!$entity) $entity = $this->entity;
$entity = DocumentationService::is_registered_entity($entity);
if(!$entity) return false;
$versions = $entity->getVersions();
$output = new ArrayList();
if($versions) {
$lang = $this->getLang();
$currentVersion = $this->getVersion();
foreach($versions as $key => $version) {
if(!$version) continue;
$linkingMode = ($currentVersion == $version) ? 'current' : 'link';
$output->push(new ArrayData(array(
'Title' => $version,
'Link' => $this->Link(implode('/',$this->Remaining), $entity->getFolder(), $version),
'LinkingMode' => $linkingMode,
'Version' => $version // separate from title, we may want to make title nicer.
)));
}
}
return $output;
public function getVersions() {
return ($this->record) ? $this->record->getEntity()->getVersions() : null;
}
/**
@ -381,8 +244,8 @@ class DocumentationViewer extends Controller {
*
* @return DataObject
*/
public function getEntities($version = false, $lang = false) {
$entities = DocumentationService::get_registered_entities($version, $lang);
public function getEntities() {
$entities = DocumentationService::get_registered_entities();
$output = new ArrayList();
$currentEntity = $this->getEntity();
@ -392,12 +255,13 @@ class DocumentationViewer extends Controller {
$mode = ($entity === $currentEntity) ? 'current' : 'link';
$folder = $entity->getFolder();
$link = $this->Link(array(), $folder, false, $lang);
$link = $entity->Link();
$content = false;
if($page = $entity->getIndexPage($version, $lang)) {
$content = DBField::create_field('HTMLText', DocumentationParser::parse($page, $link));
}
// if($page = $entity->getIndexPage()) {
// $content = DBField::create_field('HTMLText', DocumentationParser::parse($page, $link));
// }
$output->push(new ArrayData(array(
'Title' => $entity->getTitle(),
@ -415,7 +279,7 @@ class DocumentationViewer extends Controller {
/**
* Get the currently accessed entity from the site.
*
* @return false|DocumentationEntity
* @return DocumentationEntity
*/
public function getEntity() {
if($this->entity) {
@ -426,179 +290,7 @@ class DocumentationViewer extends Controller {
);
}
return false;
}
/**
* Simple way to check for existence of page of folder
* without constructing too much object state. Useful for
* generating 404 pages. Returns 0 for not a page or
* folder, returns 1 for a page and 2 for folder
*
* @return int
*/
public function locationExists() {
$entity = $this->getEntity();
if($entity) {
$has_dir = is_dir(Controller::join_links(
$entity->getPath($this->getVersion(), $this->getLang()),
implode('/', $this->Remaining)
));
if($has_dir) return 2;
$has_page = DocumentationService::find_page(
$entity,
$this->Remaining,
$this->getVersion(),
$this->getLang()
);
if($has_page) return 1;
}
return 0;
}
/**
* @return DocumentationPage
*/
function getPage() {
$entity = $this->getEntity();
if(!$entity) return false;
$version = $this->getVersion();
$lang = $this->getLang();
$absFilepath = DocumentationService::find_page(
$entity,
$this->Remaining,
$version,
$lang
);
if($absFilepath) {
$relativeFilePath = str_replace(
$entity->getPath($version, $lang),
'',
$absFilepath
);
$page = new DocumentationPage();
$page->setRelativePath($relativeFilePath);
$page->setEntity($entity);
$page->setLang($lang);
$page->setVersion($version);
return $page;
}
return false;
}
/**
* Get the related pages to the current {@link DocumentationEntity} and
* the children to those pages
*
* @todo this only handles 2 levels. Could make it recursive
*
* @return false|ArrayList
*/
function getEntityPages() {
if($entity = $this->getEntity()) {
$pages = DocumentationService::get_pages_from_folder($entity, null, self::$recursive_submenu, $this->getVersion(), $this->getLang());
if($pages) {
foreach($pages as $page) {
if(strtolower($page->Title) == "index") {
$pages->remove($page);
continue;
}
$page->LinkingMode = 'link';
$page->Children = $this->_getEntityPagesNested($page, $entity);
if (!empty($page->Children)) {
$this->currentLevelOnePage = $page;
}
}
}
return $pages;
}
return false;
}
/**
* Get all the pages under a given page. Recursive call for {@link getEntityPages()}
*
* @todo Need to rethink how to support pages which are pulling content from their children
* i.e if a folder doesn't have 2 then it will load the first file in the folder
* however it doesn't yet pass the highlighting to it.
*
* @param ArrayData CurrentPage
* @param DocumentationEntity
* @param int Depth of page in the tree
*
* @return ArrayList|false
*/
private function _getEntityPagesNested(&$page, $entity, $level = 0) {
if(isset($this->Remaining[$level])) {
// compare segment successively, e.g. with "changelogs/alpha/2.4.0-alpha",
// first comparison on $level=0 is against "changelogs",
// second comparison on $level=1 is against "changelogs/alpha", etc.
$segments = array_slice($this->Remaining, 0, $level+1);
if(strtolower(implode('/', $segments)) == strtolower(trim($page->getRelativeLink(), '/'))) {
// its either in this section or is the actual link
$page->LinkingMode = (isset($this->Remaining[$level + 1])) ? 'section' : 'current';
$relativePath = Controller::join_links(
$entity->getPath($this->getVersion(), $this->getLang()),
$page->getRelativePath()
);
if(is_dir($relativePath)) {
$children = DocumentationService::get_pages_from_folder(
$entity,
$page->getRelativePath(),
self::$recursive_submenu,
$this->getVersion(),
$this->getLang()
);
$segments = array();
for($x = 0; $x <= $level; $x++) {
$segments[] = $this->Remaining[$x];
}
foreach($children as $child) {
if(strtolower($child->Title) == "index") {
$children->remove($child);
continue;
}
$child->LinkingMode = 'link';
$child->Children = $this->_getEntityPagesNested($child, $entity, $level + 1);
}
return $children;
}
} else {
if ($page->getRelativeLink() == $this->Remaining[$level]) {
$page->LinkingMode = 'current';
}
}
}
return false;
return null;
}
/**
@ -612,7 +304,9 @@ class DocumentationViewer extends Controller {
$page = $this->getPage();
if($page) {
return DBField::create_field("HTMLText", $page->getHTML($this->getVersion(), $this->getLang()));
return DBField::create_field("HTMLText", $page->getHTML(
$this->getVersion(), $this->getLanguage()
));
}
// If no page found then we may want to get the listing of the folder.
@ -621,13 +315,7 @@ class DocumentationViewer extends Controller {
$url = $this->Remaining;
if($url && $entity) {
$pages = DocumentationService::get_pages_from_folder(
$entity,
implode('/', $url),
false,
$this->getVersion(),
$this->getLang()
);
// @todo manifest
return $this->customise(array(
'Content' => false,
@ -647,96 +335,40 @@ class DocumentationViewer extends Controller {
}
/**
* Generate a list of breadcrumbs for the user. Based off the remaining
* params in the url
* Generate a list of breadcrumbs for the user.
*
* @return ArrayList
*/
public function getBreadcrumbs() {
if(!$this->Remaining) {
$this->Remaining = array();
if($this->record) {
return $this->getManifest()->generateBreadcrumbs($this->record);
}
$pages = array_merge(array($this->entity), $this->Remaining);
$output = new ArrayList();
if($pages) {
$path = array();
$version = $this->getVersion();
$lang = $this->getLang();
foreach($pages as $i => $title) {
if($title) {
// Don't add entity name, already present in Link()
if($i > 0) $path[] = $title;
$output->push(new ArrayData(array(
'Title' => DocumentationService::clean_page_name($title),
'Link' => rtrim($this->Link($path, false, $version, $lang), "/"). "/"
)));
}
}
}
return $output;
}
/**
* @return DocumentationPage
*/
public function getPage() {
return $this->record;
}
/**
* Generate a string for the title tag in the URL.
*
* @return string
*/
public function getPageTitle() {
if($pages = $this->getBreadcrumbs()) {
$output = "";
foreach($pages as $page) {
$output = $page->Title .' &#8211; '. $output;
}
return $output;
}
return false;
return ($this->record) ? $this->record->getBreadcrumbTitle() : null;
}
/**
* Return the base link to this documentation location
* Return the base link to this documentation location.
*
* @param string $path - subfolder path
* @param string $entity - name of entity
* @param float $version - optional version
* @param string $lang - optional lang
*
* @return String
* @return string
*/
public function Link($path = false, $entity = false, $version = false, $lang = false) {
$version = ($version === null) ? $this->getVersion() : $version;
$lang = (!$lang) ? $this->getLang() : $lang;
$entity = (!$entity && $this->entity) ? $this->entity : $entity;
$action = '';
if(is_string($path)) {
$action = $path;
}
else if(is_array($path)) {
$action = implode('/', $path);
}
// check for stable version: if so, remove version from link
// (see DocumentationEntity->getRelativeLink() )
$objEntity = $this->getEntity();
if ($objEntity && $objEntity->getStableVersion() == $version) $version = '';
public function Link() {
$link = Controller::join_links(
Director::absoluteBaseURL(),
self::get_link_base(),
$entity,
($entity) ? $lang : "", // only include lang for entity - sapphire/en vs en/
($entity) ? $version :"",
$action
Config::inst()->get('DocumentationViewer', 'link_base')
);
return $link;
@ -751,7 +383,7 @@ class DocumentationViewer extends Controller {
*/
public function LanguageForm() {
$langs = $this->getLanguages();
$fields = new FieldList(
$dropdown = new DropdownField(
'LangCode',
@ -764,9 +396,7 @@ class DocumentationViewer extends Controller {
$actions = new FieldList(
new FormAction('doLanguageForm', _t('DocumentationViewer.CHANGE', 'Change'))
);
$dropdown->setDisabled(true);
return new Form($this, 'LanguageForm', $fields, $actions);
}
@ -780,27 +410,6 @@ class DocumentationViewer extends Controller {
return $this->redirect($this->Link());
}
/**
* @param string
*/
public static function set_link_base($base) {
self::$link_base = $base;
}
/**
* @return string
*/
public static function get_link_base() {
return self::$link_base;
}
/**
* @see {@link Form::FormObjectLink()}
*/
public function FormObjectLink($name) {
return $name;
}
/**
* Documentation Search Form. Allows filtering of the results by many entities
* and multiple versions.
@ -811,140 +420,11 @@ class DocumentationViewer extends Controller {
if(!DocumentationSearch::enabled()) {
return false;
}
$q = ($q = $this->getSearchQuery()) ? $q->NoHTML() : "";
$entities = $this->getSearchedEntities();
$versions = $this->getSearchedVersions();
$fields = new FieldList(
new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $q)
);
if ($entities) $fields->push(
new HiddenField('Entities', '', implode(',', array_keys($entities)))
);
if ($versions) $fields->push(
new HiddenField('Versions', '', implode(',', $versions))
);
$actions = new FieldList(
new FormAction('results', 'Search')
);
$form = new Form($this, 'DocumentationSearchForm', $fields, $actions);
$form->disableSecurityToken();
$form->setFormMethod('GET');
$form->setFormAction(self::$link_base . 'DocumentationSearchForm');
return $form;
return new DocumentationSearchForm($this);
}
/**
* Return an array of folders and titles
*
* @return array
*/
public function getSearchedEntities() {
$entities = array();
if(!empty($_REQUEST['Entities'])) {
if(is_array($_REQUEST['Entities'])) {
$entities = Convert::raw2att($_REQUEST['Entities']);
}
else {
$entities = explode(',', Convert::raw2att($_REQUEST['Entities']));
$entities = array_combine($entities, $entities);
}
}
else if($entity = $this->getEntity()) {
$entities[$entity->getFolder()] = Convert::raw2att($entity->getTitle());
}
return $entities;
}
/**
* Return an array of versions that we're allowed to return
*
* @return array
*/
public function getSearchedVersions() {
$versions = array();
if(!empty($_REQUEST['Versions'])) {
if(is_array($_REQUEST['Versions'])) {
$versions = Convert::raw2att($_REQUEST['Versions']);
$versions = array_combine($versions, $versions);
}
else {
$version = Convert::raw2att($_REQUEST['Versions']);
$versions[$version] = $version;
}
}
else if($version = $this->getVersion()) {
$version = Convert::raw2att($version);
$versions[$version] = $version;
}
return $versions;
}
/**
* Return the current search query
*
* @return HTMLText|null
*/
public function getSearchQuery() {
if(isset($_REQUEST['Search'])) {
return DBField::create_field('HTMLText', $_REQUEST['Search']);
}
}
/**
* Past straight to results, display and encode the query
*/
public function results($data, $form = false) {
$query = (isset($_REQUEST['Search'])) ? $_REQUEST['Search'] : false;
$search = new DocumentationSearch();
$search->setQuery($query);
$search->setVersions($this->getSearchedVersions());
$search->setModules($this->getSearchedEntities());
$search->setOutputController($this);
return $search->renderResults();
}
/**
* Returns an search form which allows people to express more complex rules
* and options than the plain search form.
*
* @todo client side filtering of checkable option based on the module selected.
*
* @return Form
*/
public function AdvancedSearchForm() {
$entities = DocumentationService::get_registered_entities();
return new DocumentationAdvancedSearchForm($this);
}
/**
* Check if the Advanced SearchForm can be displayed. It is enabled by
* default, to disable use:
*
* <code>
* DocumentationSearch::enable_advanced_search(false);
* </code>
*
* @return bool
*/
public function getAdvancedSearchEnabled() {
return DocumentationSearch::advanced_search_enabled();
}
/**
* Check to see if the currently accessed version is out of date or
* perhaps a future version rather than the stable edition
@ -1026,7 +506,7 @@ class DocumentationViewer extends Controller {
if($entity && isset(self::$edit_links[$entity->title])) {
// build the edit link, using the version defined
$url = self::$edit_links[$entity->title];
$version = $page->getVersion();
$version = $this->getVersion();
if($version == "trunk" && (isset($url['options']['rewritetrunktomaster']))) {
if($url['options']['rewritetrunktomaster']) {
@ -1037,10 +517,10 @@ class DocumentationViewer extends Controller {
return str_replace(
array('%entity%', '%lang%', '%version%', '%path%'),
array(
$entity->getFolder(),
$page->getLang(),
$entity->getBaseFolder(),
$this->getLanguage(),
$version,
ltrim($page->getRelativePath(), '/')
ltrim($page->getPath(), '/')
),
$url['url']
@ -1050,6 +530,27 @@ class DocumentationViewer extends Controller {
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()) : 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()) : null;
}
/**
* @return string

View File

@ -0,0 +1,32 @@
<?php
class DocumentationSearchForm extends Form {
public function __construct($controller) {
$q = ($q = $controller->getSearchQuery()) ? $q->NoHTML() : "";
$entities = $controller->getSearchedEntities();
$versions = $controller->getSearchedVersions();
$fields = new FieldList(
new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $q)
);
if ($entities) $fields->push(
new HiddenField('Entities', '', implode(',', array_keys($entities)))
);
if ($versions) $fields->push(
new HiddenField('Versions', '', implode(',', $versions))
);
$actions = new FieldList(
new FormAction('results', 'Search')
);
parent::__construct($controller, 'DocumentationSearchForm', $fields, $actions);
$this->disableSecurityToken();
$this->setFormMethod('GET');
$this->setFormAction($controller->Link('DocumentationSearchForm'));
}

View File

@ -1,24 +1,12 @@
<?php
/**
* A {@link DocumentationEntity} is created when registering a path with
* {@link DocumentationService::register()}.
*
* A {@link DocumentationEntity} represents a module or folder with
* documentation not an individual page. Individual documentation pages are
* represented by a {@link DocumentationPage}
*
* Each entity folder must have at least one language sub folder, which is.
* determined through {@link addVersion()} and should not be included in the
* $path argument.
* documentation not an individual page. Entities are loaded via
* {@link DocumentationService::register()} and individual pages are represented
* by a {@link DocumentationPage} and are loaded by the manifest.
*
*
* Versions are assumed to be in numeric format (e.g. '2.4'),
*
* They're also parsed through version_compare() in {@link getStableVersion()}
* which assumes a certain format:
*
* @see http://php.net/manual/en/function.version-compare.php
*
* @package docsviewer
* @subpackage models
*/
@ -29,92 +17,39 @@ class DocumentationEntity extends ViewableData {
* @var array
*/
private static $casting = array(
'Name' => 'Text'
'Title' => 'Text'
);
/**
* @var string $folder folder name
* @var string $title
*/
private $folder;
/**
* @var string $title nice title
*/
private $title;
protected $title;
/**
* @var array $version version numbers and the paths to each
* @var string $folder
*/
private $versions = array();
protected $folder;
/**
* @var array
* @var ArrayList $versions
*/
private $stableVersion;
/**
* @var Array $langs a list of available langauges
*/
private $langs = array();
protected $versions;
/**
* Constructor. You do not need to pass the langs to this as
* it will work out the languages from the filesystem
*
* @param string $folder folder name
* @param string $version version of this module
* @param string $path Absolute path to this module (excluding language folders)
* @param string $title
*/
public function __construct($folder, $version, $path, $title = false) {
$this->addVersion($version, $path);
$this->title = (!$title) ? $folder : $title;
public function __construct($folder, $title = false) {
$this->versions = new ArrayList();
$this->folder = $folder;
$this->title = (!$title) ? $folder : $title;
}
/**
* Return the languages which are available
*
* @return array
*/
public function getLanguages() {
return $this->langs;
}
/**
* Return whether this entity has a given language
*
* @return bool
*/
public function hasLanguage($lang) {
return (in_array($lang, $this->langs));
}
/**
* Add a langauge or languages to the entity
*
* @param Array|String languages
*/
public function addLanguage($language) {
if(is_array($language)) {
$this->langs = array_unique(array_merge($this->langs, $language));
}
else {
$this->langs[] = $language;
}
}
/**
* Get the folder name of this module
*
* @return String
*/
public function getFolder() {
return $this->folder;
}
/**
* Get the title of this module
* Get the title of this module.
*
* @return String
*/
@ -128,35 +63,22 @@ class DocumentationEntity extends ViewableData {
* @return array
*/
public function getVersions() {
return array_keys($this->versions);
return $this->versions;
}
/**
* @return string|boo
*/
public function getStableVersion() {
if(!$this->hasVersions()) return false;
if($this->stableVersion) {
return $this->stableVersion;
} else {
$sortedVersions = $this->getVersions();
usort($sortedVersions, create_function('$a,$b', 'return version_compare($a,$b);'));
return array_pop($sortedVersions);
}
}
/**
* @param String $version
*/
public function setStableVersion($version) {
if(!$this->hasVersion($version)) {
throw new InvalidArgumentException(sprintf('Version "%s" does not exist', $version));
if(!$this->hasVersions()) {
return false;
}
$this->stableVersion = $version;
$sortedVersions = $this->getVersions();
usort($sortedVersions, create_function('$a,$b', 'return version_compare($a,$b);'));
return array_pop($sortedVersions);
}
/**
@ -179,7 +101,7 @@ class DocumentationEntity extends ViewableData {
* @return bool
*/
public function hasVersion($version) {
return (isset($this->versions[$version]));
return $this->versions->find('Version', $version);
}
/**
@ -188,133 +110,65 @@ class DocumentationEntity extends ViewableData {
* @return bool
*/
public function hasVersions() {
return (sizeof($this->versions) > 0);
return $this->versions->count() > 0;
}
/**
* Add another version to this entity
*
* @param Float $version Version number
* @param String $path path to folder
* @param DocumentationEntityVersion
*/
public function addVersion($version = '', $path) {
public function addVersion($version) {
$this->versions->push($version);
$langs = scandir($path);
$available = array();
if($langs) {
foreach($langs as $key => $lang) {
if(!is_dir($path . $lang) || strlen($lang) > 2 || in_array($lang, DocumentationService::get_ignored_files(), true)) {
$lang = 'en';
}
if(!in_array($lang, $available)) {
$available[] = $lang;
}
}
}
$this->addLanguage($available);
$this->versions[$version] = $path;
return $this;
}
/**
* Remove a version from this entity
*
* @param Float $version
* @param float $version
*
*/
public function removeVersion($version = '') {
if(isset($this->versions[$version])) {
unset($this->versions[$version]);
}
public function removeVersion($version) {
$this->versions->remove('Version', $version);
return $this;
}
/**
* Return the absolute path to this documentation entity on the
* filesystem
* Return the absolute path to this documentation entity.
*
* @return string
*/
public function getPath($version = false, $lang = false) {
if(!$version) {
$version = $this->getStableVersion();
}
if(!$lang) {
$lang = 'en';
}
if($this->hasVersion($version)) {
$path = $this->versions[$version];
}
else {
$versions = $this->getVersions();
$path = $this->versions[$versions[0]];
}
return Controller::join_links($path, $lang);
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getFolder() {
return $this->folder;
}
/**
* Returns the web accessible link to this Entity
*
* @return string
*/
public function Link($version = false, $lang = false) {
public function Link() {
return Controller::join_links(
Director::absoluteBaseURL(),
$this->getRelativeLink($version, $lang)
Config::inst()->get('DocumentationViewer', 'link_base'),
$this->getFolder()
);
}
public function getRelativeLink($version = false, $lang = false) {
if(!$lang) {
$lang = 'en';
}
if($version == $this->getStableVersion()) {
$version = false;
}
return Controller::join_links(
DocumentationViewer::get_link_base(),
$this->getFolder(),
$lang,
$version
);
}
/**
* Return the summary / index text for this entity. Either pulled
* from an index file or some other summary field
*
* @return DocumentationPage
*/
public function getIndexPage($version, $lang = 'en') {
$path = $this->getPath($version, $lang);
$absFilepath = Controller::join_links($path, 'index.md');
if(file_exists($absFilepath)) {
$relativeFilePath = str_replace($path, '', $absFilepath);
$page = new DocumentationPage();
$page->setRelativePath($relativeFilePath);
$page->setEntity($this);
$page->setLang($lang);
$page->setVersion($version);
return $page;
} else {
// fall back to reading the modules README.md
}
return false;
}
/**
* @return string
*/
public function __toString() {
return sprintf('DocumentationEntity: %s)', $this->getPath());
}
}

View File

@ -0,0 +1,83 @@
<?php
/**
* @package docsviewer
*/
class DocumentationEntityLanguage extends ViewableData {
/**
* @var string
*/
protected $language;
/**
* @var DocumentationEntityVersion
*/
protected $entity;
/**
* @param DocumentationEntityVersion $version
* @param string $language
*/
public function __construct(DocumentationEntityVersion $version, $language) {
$this->entity = $version;
$this->language = $language;
}
/**
* @return string
*/
public function Link() {
return Controller::join_links($this->entity->Link(), $this->language);
}
/**
* @return string
*/
public function getVersion() {
return $this->entity->getVersion();
}
/**
* @return string
*/
public function getLanguage() {
return $this->language;
}
/**
* @return string
*/
public function getPath() {
return Controller::join_links($this->entity->getPath(), $this->language);
}
/**
* @return string
*/
public function getBasePath() {
return $this->entity->getPath();
}
/**
* @return string
*/
public function getTitle() {
return $this->entity->getTitle();
}
/**
* @return string
*/
public function getBaseFolder() {
return $this->entity->getBaseFolder();
}
/**
* @return array
*/
public function getSupportedLanguages() {
return $this->entity->getSupportedLanguages();
}
}

View File

@ -0,0 +1,130 @@
<?php
/**
* A more specific instance of a {@link DocumentationEntity}. Each instance of
* a entity will have at least one of these objects attached to encapsulate
* linking to a particular URL.
*
* Versions are assumed to be in numeric format (e.g. '2.4'),
*
* They're also parsed through version_compare() in {@link getStableVersion()}
* which assumes a certain format:
*
* @see http://php.net/manual/en/function.version-compare.php
*
* Each {@link DocumentationEntityVersion} has a list of supported language
* instances. All documentation in the docs folder must sit under a supported
* language {@link DocumentationEntityLanguage}.
*
* @package docsviewer
*/
class DocumentationEntityVersion extends ViewableData {
/**
* @var array
*/
protected $supportedLanguages = array();
/**
* @var DocumentationEntity
*/
protected $entity;
/**
* @var mixed
*/
protected $path, $version, $stable;
/**
* @param DocumentationEntity $entity
* @param string $path
* @param float $version
* @param boolean $stable
*/
public function __construct($entity, $path, $version, $stable) {
$this->entity = $entity;
$this->path = $path;
$this->version = $version;
$this->stable = $stable;
// check what languages that this instance will support.
$langs = scandir($path);
$available = array();
if($langs) {
$possible = i18n::get_common_languages(true);
$possible['en'] = true;
foreach($langs as $key => $lang) {
if(isset($possible[$lang])) {
$this->supportedLanguages[$lang] = Injector::inst()->create(
'DocumentationEntityLanguage',
$this,
$lang
);
} else {
}
}
}
}
/**
* @return string
*/
public function Link() {
if($this->stable) {
return $this->entity->Link();
}
return Controller::join_links($this->entity->Link(), $this->version);
}
/**
* Return the languages which are available for this version of the entity.
*
* @return array
*/
public function getSupportedLanguages() {
return $this->supportedLanguages;
}
/**
* Return whether this entity has a given language.
*
* @return bool
*/
public function hasLanguageSupport($lang) {
return (in_array($lang, $this->getSupportedLanguages()));
}
/**
* @return float
*/
public function getVersion() {
return $this->version;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getBaseFolder() {
return $this->entity->getFolder();
}
/**
* @return string
*/
public function getTitle() {
return $this->entity->getTitle();
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* A specific documentation folder within a {@link DocumentationEntity}.
*
* Maps to a folder on the file system.
*
* @package docsviewer
* @subpackage model
*/
class DocumentationFolder extends DocumentationPage {
/**
* @return string
*/
public function getTitle() {
return DocumentationHelper::clean_page_name($this->filename);
}
}

View File

@ -12,137 +12,42 @@
*/
class DocumentationPage extends ViewableData {
/**
* @var DocumentationEntity
*/
protected $entity;
/**
* Stores the relative path (from the {@link DocumentationEntity} to
* this page. The actual file name can be accessed via {@link $this->getFilename()}
*
* @var string
*/
protected $relativePath;
/**
* @var string
*/
protected $lang = 'en';
/**
* @var string
*/
protected $title;
/**
* @var string
*/
protected $version;
/**
* @var boolean
*/
protected $isFolder = false;
protected $summary;
/**
* @var integer
* @var DocumentationEntityLanguage
*/
protected $pagenumber = 0;
/**
* @param boolean
*/
public function setIsFolder($isFolder = false) {
$this->isFolder = $isFolder;
}
protected $entity;
/**
* @return boolean
* @var string
*/
public function getIsFolder($isFolder = false) {
return $this->isFolder;
}
protected $path, $filename;
/**
*
* @param int $number
* @param DocumentationEntityLanguage $entity
* @param string $filename
* @param string $path
*/
public function setPagenumber($number = 0) {
if (is_int($number )) $this->pagenumber = $number;
}
/**
* @return DocumentationEntity
*/
public function getEntity() {
return $this->entity;
}
/**
* @param DocumentationEntity
*/
public function setEntity($entity) {
public function __construct(DocumentationEntityLanguage $entity, $filename, $path) {
$this->filename = $filename;
$this->path = $path;
$this->entity = $entity;
}
/**
* @return string
*/
public function getRelativePath() {
return $this->relativePath;
}
/**
* @param string
*/
public function setRelativePath($path) {
$this->relativePath = $path;
}
/**
* @return string
*/
public function getExtension() {
return DocumentationService::get_extension($this->getRelativePath());
}
/**
* Absolute path including version and lang folder.
*
* @throws InvalidArgumentException
*
* @param bool $defaultFile - If this is a folder and this is set to true then getPath
* will return the path of the first file in the folder
* @return string
*/
public function getPath($defaultFile = false, $realpath = true) {
if($this->entity) {
$path = Controller::join_links(
$this->entity->getPath($this->getVersion(), $this->lang),
$this->getRelativePath()
);
if(!is_dir($path) && $realpath) $path = realpath($path);
else if($defaultFile) {
$file = DocumentationService::find_page($this->entity, explode('/', $this->getRelativePath()));
if($file) $path = $file;
}
}
else {
$path = $this->getRelativePath();
}
if(!file_exists($path)) {
throw new InvalidArgumentException(sprintf(
'Path could not be found. Module path: %s, file path: %s',
$this->entity->getPath(),
$this->getRelativePath()
));
}
return (is_dir($path)) ? rtrim($path, '/') . '/' : $path;
return DocumentationService::get_extension($this->filename);
}
/**
@ -151,110 +56,39 @@ class DocumentationPage extends ViewableData {
* @return string
*/
public function getBreadcrumbTitle($divider = ' - ') {
$pathParts = explode('/', $this->getRelativePath());
$pathParts = explode('/', $this->getRelativeLink());
// add the module to the breadcrumb trail.
array_unshift($pathParts, $this->entity->getTitle());
$titleParts = array_map(array('DocumentationService', 'clean_page_name'), $pathParts);
$titleParts = array_map(array('DocumentationHelper', 'clean_page_name'), $pathParts);
return implode($divider, $titleParts + array($this->getTitle()));
}
/**
* Returns the public accessible link for this page.
*
* @param Boolean Absolute URL (incl. domain), or relative to webroot
* @return string
* @return DocumentationEntityLanguage
*/
public function getLink($absolute = true) {
if($entity = $this->getEntity()) {
$link = $this->getRelativeLink();
$link = rtrim(DocumentationService::trim_extension_off($link), '/');
// folders should have a / on them. Looks nicer
try {
if(is_dir($this->getPath())) $link .= '/';
}
catch (Exception $e) {}
}
else {
$link = $this->getPath(true);
}
if($absolute) {
$fullLink = Controller::join_links($entity->Link($this->getVersion(), $this->lang), $link);
} else {
$fullLink = Controller::join_links($entity->getRelativeLink($this->getVersion(), $this->lang), $link);
}
return $fullLink;
}
/**
* Relative to the module base, not the webroot.
*
* @return string
*/
public function getRelativeLink() {
$link = rtrim(DocumentationService::trim_extension_off($this->getRelativePath()), '/');
// folders should have a / on them. Looks nicer
try {
if(is_dir($this->getPath())) $link .= '/';
} catch (Exception $e) {};
return $link;
}
function getLang() {
return $this->lang;
}
function setLang($lang) {
$this->lang = $lang;
}
function getVersion() {
return $this->version ? $this->version : $this->entity->getStableVersion();
}
function setVersion($version) {
$this->version = $version;
}
function setTitle($title) {
$this->title = $title;
}
function getTitle() {
return $this->title;
public function getEntity() {
return $this->entity;
}
/**
* Set a variable from the metadata field on this class
*
* @param string key
* @param mixed value
* @return string
*/
public function setMetaData($key, $value) {
$this->$key = $value;
public function getTitle() {
if($this->title) {
return $this->title;
}
return DocumentationHelper::clean_page_name($this->filename);
}
/**
* @return string
*/
public function getFilename() {
$path = rtrim($this->relativePath, '/');
try {
return (is_dir($this->getPath())) ? $path . '/' : $path;
}
catch (Exception $e) {
}
return $path;
public function getSummary() {
return $this->summary;
}
/**
@ -266,20 +100,17 @@ class DocumentationPage extends ViewableData {
*/
public function getMarkdown($removeMetaData = false) {
try {
$path = $this->getPath(true);
if($path) {
$ext = $this->getExtension();
if(empty($ext) || DocumentationService::is_valid_extension($ext)) {
if ($md = file_get_contents($path)) {
if ($this->title != 'Index') $this->getMetadataFromComments($md, $removeMetaData);
}
return $md;
}
if ($md = file_get_contents($this->path)) {
if ($this->title != 'Index') {
$this->populateMetaDataFromText($md, $removeMetaData);
}
return $md;
}
}
catch(InvalidArgumentException $e) {}
catch(InvalidArgumentException $e) {
}
return false;
}
@ -291,23 +122,65 @@ class DocumentationPage extends ViewableData {
*
* @return string
*/
public function getHTML($version, $lang = 'en') {
return DocumentationParser::parse($this, $this->entity->getRelativeLink($version, $lang));
public function getHTML() {
return DocumentationParser::parse(
$this,
$this->entity->Link()
);
}
/**
* @return string
*/
public function getRelativeLink() {
$path = str_replace($this->entity->getPath(), '', $this->path);
$url = explode('/', $path);
$url = implode('/', array_map(function($a) {
return DocumentationHelper::clean_page_url($a);
}, $url));
$url = rtrim($url, '/') . '/';
return $url;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* Returns the URL that will be required for the user to hit to view the
* given document base name.
*
* @param string $file
* @param string $path
*
* @return string
*/
public function Link() {
return Controller::join_links(
$this->entity->Link(),
$this->getRelativeLink()
);
}
/**
* get metadata from the first html block in the page, then remove the
* Return metadata from the first html block in the page, then remove the
* block on request
*
* @param DocumentationPage $md
* @param bool $remove
*/
public function getMetadataFromComments(&$md, $removeMetaData = false) {
if($md && DocumentationService::meta_comments_enabled()) {
public function populateMetaDataFromText(&$md, $removeMetaData = false) {
if($md) {
// get the text up to the first whiteline
$extPattern = "/^(.+)\n(\r)*\n/Uis";
$matches = preg_match($extPattern, $md, $block);
if($matches && $block[1]) {
$metaDataFound = false;
@ -320,36 +193,19 @@ class DocumentationPage extends ViewableData {
// check if a property exists for this key
if (property_exists(get_class(), $key)) {
$this->setMetaData($key, $meta['value'][$index]);
$this->$key = $meta['value'][$index];
$metaDataFound = true;
}
}
}
// optionally remove the metadata block (only on the page that is displayed)
// optionally remove the metadata block (only on the page that
// is displayed)
if ($metaDataFound && $removeMetaData) {
$md = preg_replace($extPattern, '', $md);
}
}
}
}
/**
* 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() {
}
/**
* Returns the previous page. Either returns the previous sibling or the
* parent of this page
*
* @return DocumentationPage
*/
public function getPreviousPage() {
}
}

View File

@ -0,0 +1,14 @@
<?php
class DocumentationBuild extends BuildTask {
public function run($request) {
$manifest = new DocumentationManifest(true);
echo "<pre>";
print_r($manifest->getPages());
echo "</pre>";
die();;
}
}

View File

@ -17,10 +17,6 @@
"silverstripe/framework": "~3.1",
"erusev/parsedown-extra": "0.1.0"
},
"repositories": [{
"type": "vcs",
"url": "https://github.com/wilr/parsedown"
}],
"suggest": {
"silverstripe/staticpublisher": "Allows publishing documentation as HTML"
}

View File

@ -57,6 +57,11 @@ html {
padding: 15px 15px 14px;
}
#sidebar .nav .current {
background: #f6f7f8;
font-weight: bold;
}
#layout {
padding-bottom: 20px;
}

View File

@ -1,3 +1,5 @@
title: Syntax Highlighting
# Syntax Highlighting
The custom Markdown parser can render custom prefixes for code blocks, and
@ -9,12 +11,12 @@ To include the syntax highlighter source, add the following to your `Documentati
```php
Requirements::javascript(THIRDPARTY_DIR .'/jquery/jquery.js');
Requirements::javascript('sapphiredocs/thirdparty/syntaxhighlighter/scripts/shCore.js');
Requirements::javascript('sapphiredocs/thirdparty/syntaxhighlighter/scripts/shBrushJScript.js');
Requirements::javascript('sapphiredocs/thirdparty/syntaxhighlighter/scripts/shBrushPHP.js');
Requirements::javascript('sapphiredocs/thirdparty/syntaxhighlighter/scripts/shBrushXML.js');
// ... any additional syntaxes you want to support
Requirements::combine_files(
'syntaxhighlighter.js',
@ -33,6 +35,3 @@ Requirements::css('sapphiredocs/thirdparty/syntaxhighlighter/styles/shCore.css')
Requirements::css('sapphiredocs/thirdparty/syntaxhighlighter/styles/shCoreDefault.css');
Requirements::css('sapphiredocs/thirdparty/syntaxhighlighter/styles/shThemeRDark.css');
```
You can overload the `DocumentationViewer` class and add a custom route through `Director::addRule()`
if you prefer not to modify the module file.

View File

@ -1,62 +0,0 @@
# Writing Documentation
The files have to end with the __.md__ or __.markdown__ extension. The
documentation viewer will automatically replace hyphens (-) with spaces.
my-documentation-file.md
Translates to:
My documentation file
The module also support number prefixing for specifying the order of pages in
the index pages and navigation trees.
03-foo.md
1-bar.md
4-baz.md
Will be output as the following in the listing views.
Bar
Foo
Baz
## Localization
All documentation folder should be localized. Even if you do not plan on supporting
multiple languages you need to write your documentation in a 'en' subfolder
/module/docs/en/
## Syntax
Documentation should be written in markdown with an `.md` extension attached.
To view the syntax for page formatting check out [Daring Fireball](http://daringfireball.net/projects/markdown/syntax).
To see how to use the documentation from examples, I recommend opening up this
file in your text editor and playing around. As these files are plain text, any
text editor will be able to open and write markdown files.
## Creating Hierarchy
The document viewer supports a hierarchical folder structure so you can categorize
documentation and create topics.
## Directory Listing
Each folder you create should also contain a __index.md__ file which contains
an overview of the module and related links. If no index is available, the
default behaviour is to display an ordered list of links.
## Table of Contents
The table of contents on each module page is generated based on where and what
headers you use.
## Images and Files
If you want to attach images and other assets to a page you need to bundle those
in a directory called _images at the same level as your documentation.

View File

@ -2,9 +2,9 @@
## Registering what to document
By default the documentation system will parse all the directories in your project
and include the documentation. If you want to only specify a few folders you can
disable it and register your paths manually
By default the documentation system will parse all the directories in your
project and include the documentation. If you want to only specify a few folders
you can disable it and register your paths manually
:::php
// turns off automatic parsing of filesystem
@ -65,13 +65,6 @@ Custom metadata can be added to the head of the MarkDown file like this:
Make sure to add an empty line to separate the metadata from the content of
the file.
You now need to explicitly enable the use of metadata by adding the following to
your _config.php:
```php
DocumentationService::enable_meta_comments();
```
**Note:** SilverStripe needs to read the contents of each page to retrieve the
metadata. This is expensive, so if you do not plan to use custom sorting,
do not enable this feature:
@ -101,4 +94,65 @@ Basically all DocumentationPage properties can be added to the metadata comment
block. Beware that the outcome isn't always predictable. Adding a title
property to the block will change the menu title, but the breadcrumbs
are at this time not yet supported.
The files have to end with the __.md__ or __.markdown__ extension. The
documentation viewer will automatically replace hyphens (-) with spaces.
my-documentation-file.md
Translates to:
My documentation file
The module also support number prefixing for specifying the order of pages in
the index pages and navigation trees.
03-foo.md
1-bar.md
4-baz.md
Will be output as the following in the listing views.
Bar
Foo
Baz
## Localization
All documentation folder should be localized. Even if you do not plan on supporting
multiple languages you need to write your documentation in a 'en' subfolder
/module/docs/en/
## Syntax
Documentation should be written in markdown with an `.md` extension attached.
To view the syntax for page formatting check out [Daring Fireball](http://daringfireball.net/projects/markdown/syntax).
To see how to use the documentation from examples, I recommend opening up this
file in your text editor and playing around. As these files are plain text, any
text editor will be able to open and write markdown files.
## Creating Hierarchy
The document viewer supports a hierarchical folder structure so you can categorize
documentation and create topics.
## Directory Listing
Each folder you create should also contain a __index.md__ file which contains
an overview of the module and related links. If no index is available, the
default behaviour is to display an ordered list of links.
## Table of Contents
The table of contents on each module page is generated based on where and what
headers you use.
## Images and Files
If you want to attach images and other assets to a page you need to bundle those
in a directory called _images at the same level as your documentation.

View File

@ -84,7 +84,7 @@ By default, the documentation is available in `dev/docs`. If you want it to
live on the webroot instead of a subfolder or on another url address, add the
following configuration to your _config.php file:
DocumentationViewer::set_link_base('');
Config::inst()->update('DocumentationViewer', 'link_base', '');
Director::addRules(1, array(
'$Action' => 'DocumentationViewer',

View File

@ -108,7 +108,8 @@ Currently this is not supported, as all HTML is generated on the fly.
### Can I contribute to the parser and rendering project?
Of course, the `docsviewer` code is BSD licensed - we're looking forward to your contributions!
Of course, the `docsviewer` code is BSD licensed - we're looking forward to your
contributions!
## Related ##

24
docs/en/statichtml.md Normal file
View File

@ -0,0 +1,24 @@
title: Publishing Static Files
# HTML Publishing
If you wish to generate a truly static version of your documentation after it
has been rendered through the website, add the [Static Publisher](https://github.com/silverstripe-labs/silverstripe-staticpublisher)
module to your documentation project and set the following configuration in your
applications config.yml:
```
StaticExporter:
extensions:
- DocumentationStaticPublisherExtension
```
If you don't plan on using static publisher for anything else and you have the
cms module installed, make sure you disable the CMS from being published.
Again, in your applications config.yml file
```
StaticExporter:
disable_sitetree_export: true
```

View File

@ -77,7 +77,7 @@
*
* Transform a #table-of-contents div to a nested list
*/
if($("#content-column").length > 0) {
if($("#table-contents-holder").length > 0) {
var toc = '<div id="table-of-contents" class="open">' +
'<h4>Table of contents<span class="updown">&#9660;</span></h4><ul style="display: none;">';
@ -85,7 +85,7 @@
var pageURL = window.location.href.replace(/#[a-zA-Z0-9\-\_]*/g, '');
var itemCount = 0;
$('#content-column h1[id], #content-column h2[id], #content-column h3[id], #content-column h4[id]').each(function(i) {
$('#content h1[id], #content h2[id], #content h3[id], #content h4[id]').each(function(i) {
var current = $(this);
var tagName = current.prop("tagName");
if(typeof tagName == "String") tagName = tagName.toLowerCase();
@ -99,15 +99,15 @@
toc += '</ul></div>';
// Table of content location
var title = $('#content-column h1:first');
var title = $('#content h1:first');
if (title.length > 0) {
title.after(toc);
} else {
var breadcrums = $('#content-column .doc-breadcrumbs');
var breadcrums = $('#content .doc-breadcrumbs');
if (breadcrums.length > 0) {
breadcrums.after(toc);
} else {
$('#content-column').prepend(toc);
$('#content').prepend(toc);
}
}
@ -133,7 +133,7 @@
*/
var url = window.location.href;
$("#content-column h1[id], #content-column h2[id], #content-column h3[id], #content-column h4[id], #content-column h5[id], #content-column h6[id]").each(function() {
$("#content h1[id], #content h2[id], #content h3[id], #content h4[id], #content h5[id], #content h6[id]").each(function() {
var link = '<a class="heading-anchor-link" title="Link to this section" href="'+ url + '#' + $(this).attr('id') + '">&para;</a>';
$(this).append(' ' + link);
});

View File

@ -1,7 +1,7 @@
<div class="doc-breadcrumbs">
<p>
<% loop Breadcrumbs %>
<a href="$Link">$Title</a> <% if Last %><% else %>&rsaquo;<% end_if %>
<% end_loop %>
</p>
<p>
<% loop Breadcrumbs %>
<a href="$Link">$Title</a> <% if Last %><% else %>&rsaquo;<% end_if %>
<% end_loop %>
</p>
</div>

View File

@ -0,0 +1,11 @@
<% if NextPage || PreviousPage %>
<div class="next-prev">
<% if PreviousPage %>
<p class="prev-link"><a href="$PreviousPage.Link">$PreviousPage.Title</a></p>
<% end_if %>
<% if NextPage %>
<p class="next-link"><a href="$NextPage.Link">$NextPage.Title</a></p>
<% end_if %>
</div>
<% end_if %>

View File

@ -1,16 +1,20 @@
<div id="sidebar" class="box">
<ul class="nav">
<% loop Entities %>
<li><a href="$Link" class="$LinkingMode top">$Title <% if IsFolder %><span class="is-folder">&#9658;</span><% end_if %></a>
<% if LinkingMode == current %>
<% if Children %>
<ul>
<% loop Children %>
<li><a href="$Link" class="$LinkingMode">$Title</a></li>
<% end_loop %>
</ul><% end_if %>
<% end_if %>
</li>
<% if DefaultEntity %>
<% else %>
<li><a href="$Link" class="$LinkingMode top">$Title <% if IsFolder %><span class="is-folder">&#9658;</span><% end_if %></a>
<% if LinkingMode == current %>
<% if Children %>
<ul>
<% loop Children %>
<li><a href="$Link" class="$LinkingMode">$Title</a></li>
<% end_loop %>
</ul><% end_if %>
<% end_if %>
</li>
<% end_if %>
<% end_loop %>
</ul>
</div>

View File

@ -0,0 +1 @@
<div id="table-contents-holder"></div>

View File

@ -1,19 +0,0 @@
<% if VersionWarning %>
<% include DocumentationVersion_warning %>
<% end_if %>
<div id="documentation-page">
<div id="content-column">
<% if Breadcrumbs %>
<% include DocumentationBreadcrumbs %>
<% end_if %>
$Content
<% if EditLink %>
<% include DocumentationEditLink %>
<% end_if %>
</div>
</div>
<% include DocumentationComments %>

View File

@ -0,0 +1,17 @@
<div class="box">
<% if VersionWarning %>
<% include DocumentationVersion_warning %>
<% end_if %>
<h2>$Title</h2>
<% include DocumentationTableContents %>
<% loop Children %>
<ul>
<li><a href="$Link">$Title</a></li>
</ul>
<% end_loop %>
<% include DocumentationNextPrevious %>
</div>

View File

@ -0,0 +1,20 @@
<div id="documentation-page" class="box">
<% if VersionWarning %>
<% include DocumentationVersion_warning %>
<% end_if %>
<% if Breadcrumbs %>
<% include DocumentationBreadcrumbs %>
<% end_if %>
$Content
<% include DocumentationNextPrevious %>
<% if EditLink %>
<% include DocumentationEditLink %>
<% end_if %>
<% include DocumentationComments %>
</div>

View File

@ -1,15 +0,0 @@
<div id="module-home" class="box">
<% if VersionWarning %>
<% include DocumentationVersion_warning %>
<% end_if %>
<% if Content %>
$Content
<% if EditLink %>
<% include DocumentationEditLink %>
<% end_if %>
<% else %>
<h2>$Title</h2>
<% end_if %>
</div>

View File

@ -1,13 +0,0 @@
<div id="home">
<h2><% _t('DOCUMENTEDMODULES', 'Documented Modules') %></h2>
<% if Entities %>
<% loop Entities %>
<div class="module">
<h3><a href="$Link">$Title</a></h3>
</div>
<% end_loop %>
<% else %>
<p><% _t('NOMODULEDOCUMENTATION', 'No modules with documentation installed could be found.') %></p>
<% end_if %>
</div>

View File

@ -1,48 +1,45 @@
<div id="documentation-page">
<div id="content-column">
<p>Your search for <strong>&quot;$Query.XML&quot;</strong> found $TotalResults result<% if TotalResults != 1 %>s<% end_if %>.</p>
<% if AdvancedSearchEnabled %>
<div id="documentation-page" class="box">
<p>Your search for <strong>&quot;$Query.XML&quot;</strong> found $TotalResults result<% if TotalResults != 1 %>s<% end_if %>.</p>
<% if AdvancedSearchEnabled %>
<h4><% _t('ADVANCEDSEARCH', 'Advanced Search') %></h4>
$AdvancedSearchForm
$AdvancedSearchForm
<% end_if %>
<% if Results %>
<p>Showing page $ThisPage of $TotalPages</p>
<% loop Results %>
<h2><a href="$Link"><% if BreadcrumbTitle %>$BreadcrumbTitle<% else %>$Title<% end_if %></a></h2>
<p>$Content.LimitCharacters(200)</p>
<% end_loop %>
<% if SearchPages %>
<ul class="pagination">
<% if PrevUrl = false %><% else %>
<li class="prev"><a href="$PrevUrl">Prev</a></li>
<% end_if %>
<% loop SearchPages %>
<% if IsEllipsis %>
<li class="ellipsis">...</li>
<% else %>
<% if Current %>
<li class="active"><strong>$PageNumber</strong></li>
<% else %>
<li><a href="$Link">$PageNumber</a></li>
<% end_if %>
<% end_if %>
<% end_loop %>
<% if NextUrl = false %>
<% if Results %>
<p>Showing page $ThisPage of $TotalPages</p>
<% loop Results %>
<h2><a href="$Link"><% if BreadcrumbTitle %>$BreadcrumbTitle<% else %>$Title<% end_if %></a></h2>
<p>$Content.LimitCharacters(200)</p>
<% end_loop %>
<% if SearchPages %>
<ul class="pagination">
<% if PrevUrl = false %><% else %>
<li class="prev"><a href="$PrevUrl">Prev</a></li>
<% end_if %>
<% loop SearchPages %>
<% if IsEllipsis %>
<li class="ellipsis">...</li>
<% else %>
<li class="next"><a href="$NextUrl">Next</a></li>
<% end_if %>
</ul>
<% end_if %>
<% else %>
<p>No Results</p>
<% if Current %>
<li class="active"><strong>$PageNumber</strong></li>
<% else %>
<li><a href="$Link">$PageNumber</a></li>
<% end_if %>
<% end_if %>
<% end_loop %>
<% if NextUrl = false %>
<% else %>
<li class="next"><a href="$NextUrl">Next</a></li>
<% end_if %>
</ul>
<% end_if %>
</div>
<% else %>
<p>No Results</p>
<% end_if %>
</div>

View File

@ -1,34 +0,0 @@
<?php
/**
* @package docsviewer
* @subpackage tests
*/
class DocumentationEntityTest extends SapphireTest {
function testDocumentationEntityAccessing() {
$entity = new DocumentationEntity('docs', '1.0', DOCSVIEWER_PATH .'/tests/docs/', 'My Test');
$this->assertEquals($entity->getTitle(), 'My Test');
$this->assertEquals($entity->getVersions(), array('1.0'));
$this->assertEquals($entity->getLanguages(), array('en', 'de'));
$this->assertEquals($entity->getFolder(), 'docs');
$this->assertTrue($entity->hasVersion('1.0'));
$this->assertFalse($entity->hasVersion('2.0'));
$this->assertTrue($entity->hasLanguage('en'));
$this->assertFalse($entity->hasLanguage('fr'));
}
function testgetStableVersion() {
$entity = new DocumentationEntity('docs', '1.0', DOCSVIEWER_PATH. '/tests/docs/', 'My Test');
$entity->addVersion('1.1', DOCSVIEWER_PATH. '/tests/docs-v2.4/');
$entity->addVersion('0.0', DOCSVIEWER_PATH. '/tests/docs-v3.0/');
$this->assertEquals('1.1', $entity->getStableVersion(), 'Automatic version sorting');
$entity = new DocumentationEntity('docs', '1.0', DOCSVIEWER_PATH. '/tests/docs/', 'My Test');
$entity->addVersion('1.1.', DOCSVIEWER_PATH .'/tests/docs-v2.4/');
$entity->setStableVersion('1.0');
$this->assertEquals('1.0', $entity->getStableVersion(), 'Manual setting');
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
*
*/
class DocumentationManifestTests extends SapphireTest {
private $manifest, $pages;
public function setUpOnce() {
parent::setUpOnce();
$this->origEnabled = DocumentationService::automatic_registration_enabled();
DocumentationService::set_automatic_registration(false);
$this->origModules = DocumentationService::get_registered_entities();
$this->origLinkBase = Config::inst()->get('DocumentationViewer', 'link_base');
Config::inst()->update('DocumentationViewer', 'link_base', 'dev/docs/');
foreach($this->origModules as $module) {
DocumentationService::unregister($module->getFolder());
}
// We set 3.0 as current, and test most assertions against 2.4 - to avoid 'current' rewriting issues
DocumentationService::register("testdocs", DOCSVIEWER_PATH . "/tests/docs/", '2.3');
DocumentationService::register("testdocs", DOCSVIEWER_PATH . "/tests/docs-v2.4/", '2.4', 'Doc Test', true);
DocumentationService::register("testdocs", DOCSVIEWER_PATH . "/tests/docs-v3.0/", '3.0', 'Doc Test');
$this->manifest = new DocumentationManifest(true);
$this->pages = $this->manifest->getPages();
}
public function tearDownOnce() {
parent::tearDownOnce();
DocumentationService::unregister("testdocs");
DocumentationService::set_automatic_registration($this->origEnabled);
Config::inst()->update('DocumentationViewer', 'link_base', $this->origLinkBase);
}
/**
* Check that the manifest matches what we'd expect.
*/
public function testRegenerate() {
$match = array(
'dev/docs/testdocs/2.3/de/',
'dev/docs/testdocs/2.3/de/german/',
'dev/docs/testdocs/2.3/de/test/',
'dev/docs/testdocs/2.3/en/',
'dev/docs/testdocs/2.3/en/sort/',
'dev/docs/testdocs/2.3/en/subfolder/',
'dev/docs/testdocs/2.3/en/test/',
'dev/docs/testdocs/2.3/en/sort/basic/',
'dev/docs/testdocs/2.3/en/sort/some-page/',
'dev/docs/testdocs/2.3/en/sort/intermediate/',
'dev/docs/testdocs/2.3/en/sort/another-page/',
'dev/docs/testdocs/2.3/en/sort/advanced/',
'dev/docs/testdocs/2.3/en/subfolder/subpage/',
'dev/docs/testdocs/2.3/en/subfolder/subsubfolder/',
'dev/docs/testdocs/2.3/en/subfolder/subsubfolder/subsubpage/',
'dev/docs/testdocs/en/',
'dev/docs/testdocs/en/test/',
'dev/docs/testdocs/3.0/en/',
'dev/docs/testdocs/3.0/en/changelog/',
'dev/docs/testdocs/3.0/en/tutorials/',
'dev/docs/testdocs/3.0/en/empty/'
);
$this->assertEquals($match, array_keys($this->pages));
}
public function testGetNextPage() {
}
public function testGetPreviousPage() {
}
public function testGetPage() {
}
}

View File

@ -4,10 +4,9 @@
* @package docsviewer
* @subpackage tests
*/
class DocumentationPageTest extends SapphireTest {
function testGetLink() {
public function testGetLink() {
$entity = new DocumentationEntity('testmodule', null, DOCSVIEWER_PATH .'/tests/docs/');
$page = new DocumentationPage();
@ -43,8 +42,7 @@ class DocumentationPageTest extends SapphireTest {
$this->assertStringEndsWith('versionlinks/en/1/test', $page->Link);
}
function testGetRelativePath() {
public function testGetRelativePath() {
$page = new DocumentationPage();
$page->setRelativePath('test.md');
$page->setEntity(new DocumentationEntity('mymodule', null, DOCSVIEWER_PATH . '/tests/docs/'));
@ -58,7 +56,7 @@ class DocumentationPageTest extends SapphireTest {
$this->assertEquals('subfolder/subpage.md', $page->getRelativePath());
}
function testGetPath() {
public function testGetPath() {
$absPath = DOCSVIEWER_PATH .'/tests/docs/';
$page = new DocumentationPage();
$page->setRelativePath('test.md');
@ -73,7 +71,7 @@ class DocumentationPageTest extends SapphireTest {
$this->assertEquals($absPath . 'en/subfolder/subpage.md', $page->getPath());
}
function testGetBreadcrumbTitle() {
public function testGetBreadcrumbTitle() {
$entity = new DocumentationEntity('testmodule', null, DOCSVIEWER_PATH . '/tests/docs/');
$page = new DocumentationPage();

View File

@ -12,14 +12,16 @@ class DocumentationViewerTest extends FunctionalTest {
protected $autoFollowRedirection = false;
function setUpOnce() {
public function setUpOnce() {
parent::setUpOnce();
$this->origEnabled = DocumentationService::automatic_registration_enabled();
DocumentationService::set_automatic_registration(false);
$this->origModules = DocumentationService::get_registered_entities();
$this->origLinkBase = DocumentationViewer::get_link_base();
DocumentationViewer::set_link_base('dev/docs/');
$this->origLinkBase = Config::inst()->get('DocumentationViewer', 'link_base');
Config::inst()->update('DocumentationViewer', 'link_base', 'dev/docs/');
foreach($this->origModules as $module) {
DocumentationService::unregister($module->getFolder());
}
@ -33,12 +35,13 @@ class DocumentationViewerTest extends FunctionalTest {
DocumentationService::register("DocumentationViewerAltModule2", DOCSVIEWER_PATH . "/tests/docs-search/", '1.0');
}
function tearDownOnce() {
public function tearDownOnce() {
parent::tearDownOnce();
DocumentationService::unregister("DocumentationViewerTests");
DocumentationService::set_automatic_registration($this->origEnabled);
DocumentationViewer::set_link_base($this->origLinkBase);
Config::inst()->update('DocumentationViewer', 'link_base', $this->origLinkBase);
}
/**

View File

@ -0,0 +1 @@
# Failure

2
tests/docs/de/german.md Normal file
View File

@ -0,0 +1,2 @@
# German