FEATURE: added support to register versions and modules manually and disable the automatic includsion. FEATURE: added support for multiple versions and languages in the documentation. ENHANCEMENT: added toolbox to view module docs on pages and lots of other new templates

This commit is contained in:
Will Rossiter 2010-06-24 14:22:41 +00:00
parent e11837763b
commit a07855109f
25 changed files with 1368 additions and 353 deletions

View File

@ -0,0 +1,182 @@
<?php
/**
* A wrapper for a documentation entity which is created when registering the
* path with {@link DocumentationService::register()}. This refers to a whole package
* rather than a specific page but if we need page options we may need to introduce
* a class for that.
*
* @package sapphiredocs
*/
class DocumentationEntity extends ViewableData {
static $casting = array(
'Name' => 'Text'
);
/**
* @var String $module folder name
*/
private $moduleFolder;
/**
* @var String $title nice title
*/
private $title;
/**
* @var Array $version version numbers and the paths to each
*/
private $versions = array();
/**
* @var Array $langs a list of available langauges
*/
private $langs = array();
/**
* Constructor. You do not need to pass the langs to this as
* it will work out the languages from the filesystem
*
* @param String $module name of module
* @param String $version version of this module
* @param String $path path to this module
*/
function __construct($module, $version = '', $path, $title = false) {
$this->addVersion($version, $path);
$this->title = (!$title) ? $this->module : $title;
$this->moduleFolder = $module;
}
/**
* 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 getModuleFolder() {
return $this->moduleFolder;
}
/**
* Get the title of this module
*
* @return String
*/
public function getTitle() {
return $this->title;
}
/**
* Return the versions which are available
*
* @return Array
*/
public function getVersions() {
return array_keys($this->versions);
}
/**
* Return whether we have a given version of this entity
*
* @return bool
*/
public function hasVersion($version) {
return (isset($this->versions[$version]));
}
/**
* Return whether we have any versions at all0
*
* @return bool
*/
public function hasVersions() {
return (sizeof($this->versions) > 0);
}
/**
* Add another version to this entity
*
* @param Float $version Version number
* @param String $path path to folder
*/
public function addVersion($version = '', $path) {
// determine the langs in this path
$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;
}
/**
* Remove a version from this entity
*
* @param Float $version
*/
public function removeVersion($version = '') {
if(isset($this->versions[$version])) {
unset($this->versions[$version]);
}
}
/**
* Return the path to this documentation entity
*
* @return String
*/
public function getPath($version = false, $lang = false) {
if(!$version) $version = '';
if(!$lang) $lang = 'en';
if(!$this->hasVersion($version)) $path = array_pop($this->versions);
else $path = $this->versions[$version];
return $path . $lang .'/';
}
}

View File

@ -0,0 +1,153 @@
<?php
/**
* Wrapper for MarkdownUltra parsing in the template and related functionality for
* parsing paths and documents
*
* @package sapphiredocs
*/
class DocumentationParser {
/**
* Parse a given path to the documentation for a file. Performs a case insensitive
* lookup on the file system. Automatically appends the file extension to one of the markdown
* extensions as well so /install/ in a web browser will match /install.md or /INSTALL.md
*
* @param String $module path to a module
* @param Array path of urls. Should be folders, last one is a page
*
* @return HTMLText
*/
public static function parse($module, $path) {
require_once('../sapphiredocs/thirdparty/markdown.php');
if($content = self::find_page($module, $path)) {
$content = Markdown(file_get_contents($content));
return DBField::create('HTMLText', $content);
}
return false;
}
/**
* 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 String $entity path to the entity
* @param Array $path path to the file in the entity
*
* @return String|false - File path
*/
private static function find_page($entity, $path) {
return self::find_page_recursive($entity, $path);
}
/**
* Recursive function for finding the goal
*/
private static function find_page_recursive($base, $goal) {
$handle = opendir($base);
$name = strtolower(array_shift($goal));
if(!$name) $name = 'index';
if($handle) {
$extensions = DocumentationService::get_valid_extensions();
while (false !== ($file = readdir($handle))) {
if(in_array($file, DocumentationService::get_valid_extensions())) continue;
$formatted = strtolower($file);
// if the name has a . then take the substr
$formatted = ($pos = strrpos($formatted, '.')) ? substr($formatted, 0, $pos) : $formatted;
$name = ($dot = strrpos($formatted, '.')) ? substr($name, 0, $dot) : $name;
// the folder is the one that we are looking for.
if($name == $formatted) {
if(is_dir($base . $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 . $file, $goal);
}
else {
// recurse but check for an index.md file next time around
return self::find_page_recursive($base . $file, array('index'));
}
}
else {
// goal state. End of recursion
$result = $base .'/'. $file;
return $result;
}
}
}
}
closedir($handle);
}
/**
* 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_ireplace(array('-', '_'), ' ', $name);
// remove extension
$hasExtension = strpos($name, '.');
if($hasExtension !== false && $hasExtension > 0) {
$name = substr($name, 0, $hasExtension);
}
// convert first letter
return ucfirst($name);
}
/**
* Return the children from a given module. Used for building the tree of the page
*
* @param String module name
*
* @return DataObjectSet
*/
public static function get_pages_from_folder($folder) {
$handle = opendir($folder);
$output = new DataObjectSet();
if($handle) {
$extensions = DocumentationService::get_valid_extensions();
$ignore = DocumentationService::get_ignored_files();
while (false !== ($file = readdir($handle))) {
if(!in_array($file, $ignore)) {
$file = strtolower($file);
$clean = ($pos = strrpos($file, '.')) ? substr($file, 0, $pos) : $file;
$output->push(new ArrayData(array(
'Title' => self::clean_page_name($file),
'Filename' => $clean,
'Path' => $folder . $file .'/'
)));
}
}
}
return $output;
}
}

View File

@ -0,0 +1,342 @@
<?php
/**
* DocumentationService
*
* Handles the management of the documentation services delivered by the module.
* Includes registering which components to document and handles the entities being
* documented
*
* @todo - unregistering a lang / version from site does not update the registered_* arrays
* - handle modules (rather than core) differently
* @package sapphiredocs
*/
class DocumentationService {
/**
* A mapping of know / popular languages to nice titles.
*
* @var Array
*/
private static $language_mapping = array(
'en' => 'English',
'fr' => 'French',
'de' => 'German'
);
/**
* Files to ignore from any documentation listing.
*
* @var Array
*/
private static $ignored_files = array('.', '..', '.DS_Store', '.svn', '.git', 'assets', 'themes');
/**
* Set the ignored files list
*
* @param Array
*/
public function set_ignored_files($files) {
self::$ignored_files = $files;
}
/**
* Return the list of files which are ignored
*
* @return Array
*/
public function get_ignored_files() {
return self::$ignored_files;
}
/**
* Case insenstive values to use as extensions on markdown pages.
*
* @var Array
*/
public static $valid_markdown_extensions = array('.md', '.txt', '.markdown');
/**
* Return the allowed extensions
*
* @return Array
*/
public static function get_valid_extensions() {
return self::$valid_markdown_extensions;
}
/**
* Registered modules to include in the documentation. Either pre-filled by the
* automatic filesystem parser or via {@link DocumentationService::register()}. Stores
* {@link DocumentEntity} objects which contain the languages and versions of each module.
*
* You can remove registered modules using {@link DocumentationService::unregister()}
*
* @var Array
*/
private static $registered_modules = array();
/**
* Major Versions store. We don't want to register all versions of every module in
* the documentation but for sapphire/cms and overall we need to register major
* versions via {@link DocumentationService::register}
*
* @var Array
*/
private static $major_versions = array();
/**
* Return the major versions
*
* @return Array
*/
public static function get_major_versions() {
return self::$major_versions;
}
/**
* Check to see if a given language is registered in the system
*
* @param string
* @return bool
*/
public static function is_registered_language($lang) {
$langs = self::get_registered_languages();
return (isset($langs[$lang]));
}
/**
* Get all the registered languages. Optionally limited to a module. Includes
* the nice titles
*
* @return Array
*/
public static function get_registered_languages($module = false) {
$langs = array();
if($module) {
if(isset(self::$registered_modules[$module])) {
$langs = self::$registered_modules[$module]->getLanguages();
}
}
else if($modules = self::get_registered_modules()) {
foreach($modules as $module) {
$langs = array_unique(array_merge($langs, $module->getLanguages()));
}
}
$output = array();
foreach($langs as $lang) {
$output[$lang] = self::get_language_title($lang);
}
return $output;
}
/**
* Returns all the registered versions in the system. Optionally only
* include versions from a module.
*
* @param String $module module to check for versions
* @return array
*/
public static function get_registered_versions($module = false) {
if($module) {
if(isset($registered_modules[$module])) {
return $registered_modules[$module]->getVersions();
}
else {
return false;
}
}
return self::$major_versions;
}
/**
* Should generation of documentation categories be automatic. If this
* is set to true then it will generate documentation sections (modules) from
* the filesystem. This can be slow and also some projects may want to restrict
* to specific project folders (rather than everything).
*
* You can also disable or remove a given folder from registration using
* {@link DocumentationService::unregister()}
*
* @see DocumentationService::$registered_modules
* @see DocumentationService::set_automatic_registration();
*
* @var bool
*/
private static $automatic_registration = true;
/**
* Set automatic registration of modules and documentation folders
*
* @see DocumentationService::$automatic_registration
* @param bool
*/
public static function set_automatic_registration($bool = true) {
self::$automatic_registration = $bool;
}
/**
* Is automatic registration of modules enabled.
*
* @return bool
*/
public static function automatic_registration_enabled() {
return self::$automatic_registration;
}
/**
* Return the modules which are listed for documentation. Optionally only get
* modules which have a version or language given
*
* @return array
*/
public static function get_registered_modules($version = false, $lang = false) {
$output = array();
if($modules = self::$registered_modules) {
if($version || $lang) {
foreach($modules as $module) {
if(self::is_registered_module($module->getModuleFolder(), $version, $lang)) {
$output[] = $module;
}
}
}
else {
$output = $modules;
}
}
return $output;
}
/**
* Check to see if a module is registered with the documenter
*
* @param String $module module name
* @param String $version version
* @param String $lang language
*
* @return DocumentationEntity $module the registered module
*/
public static function is_registered_module($module, $version = false, $lang = false) {
if(isset(self::$registered_modules[$module])) {
$module = self::$registered_modules[$module];
if($lang && !$module->hasLanguage($lang)) return false;
if($version && !$module->hasVersion($version)) return false;
return $module;
}
return false;
}
/**
* Register a module to be included in the documentation. To unregister a module
* use {@link DocumentationService::unregister()}. Must include the trailing slash
*
* @param String $module Name of module to register
* @param String $path Path to documentation root.
* @param Float $version Version of module.
* @param String $title Nice title to use
* @param bool $major is this a major release
*/
public static function register($module, $path, $version = '', $title = false, $major = false) {
// add the module to the registered array
if(!isset(self::$registered_modules[$module])) {
// module is completely new
$entity = new DocumentationEntity($module, $version, $path, $title);
self::$registered_modules[$module] = $entity;
}
else {
// module exists so add the version to it
$entity = self::$registered_modules[$module];
$entity->addVersion($version, $path);
}
if($major) {
if(!$version) $version = '';
if(!in_array($version, self::$major_versions)) {
self::$major_versions[] = $version;
}
}
}
/**
* Unregister a module from being included in the documentation. Useful
* for keeping {@link DocumentationService::$automatic_registration} enabled
* but disabling modules which you do not want to show. Combined with a
* {@link Director::isLive()} you can hide modules you don't want a client to see.
*
* If no version or lang specified then the whole module is removed. Otherwise only
* the specified version of the documentation.
*
* @param String $module
* @param String $version
*
* @return bool
*/
public static function unregister($module, $version = '') {
if(isset(self::$registered_modules[$module])) {
$module = self::$registered_modules[$module];
if($version) {
$module->removeVersion($version);
}
else {
// only given a module so unset the whole module
unset(self::$registered_modules[$module]);
}
return true;
}
return false;
}
/**
* Register the docs from off a file system if automatic registration is turned on.
*/
public static function load_automatic_registration() {
if(self::automatic_registration_enabled()) {
$modules = scandir(BASE_PATH);
if($modules) {
foreach($modules as $key => $module) {
if(is_dir(BASE_PATH .'/'. $module) && !in_array($module, self::$ignored_files, true)) {
// check to see if it has docs
$docs = BASE_PATH .'/'. $module .'/docs/';
if(is_dir($docs)) {
self::register($module, $docs, '', $module, 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) {
return (isset(self::$language_mapping[$lang])) ? _t("DOCUMENTATIONSERVICE.LANG-$lang", self::$language_mapping[$lang]) : $lang;
}
}

View File

@ -3,277 +3,420 @@
/** /**
* Documentation Viewer. * Documentation Viewer.
* *
* Reads the bundled markdown files from docs/ folders and displays output in a formatted page at /dev/docs/. * Reads the bundled markdown files from documentation folders and displays the output (either
* For more documentation on how to use this class see the documentation online in /dev/docs/ or in the * via markdown or plain text)
* /sapphiredocs/docs folder *
* For more documentation on how to use this class see the documentation in /sapphiredocs/docs folder
*
* To view the documentation in the browser use:
*
* http://yoursite.com/dev/docs/ Which is locked to ADMIN only
*
* @todo - Add ability to have docs on the front end as the main site.
* - Fix Language Selector (enabling it troubles the handleRequest when submitting)
* - SS_HTTPRequest when we ask for 10 params it gives us 10. Could be 10 blank ones.
* It would mean I could save alot of code if it only gave back an array of size X
* up to a maximum of 10...
* *
* @author Will Rossiter <will@silverstripe.com>
* @package sapphiredocs * @package sapphiredocs
*/ */
class DocumentationViewer extends Controller { class DocumentationViewer extends Controller {
static $url_handlers = array( static $allowed_actions = array(
'' => 'index', 'LanguageForm',
'$Module/$Page/$OtherPage' => 'parse' 'doLanguageForm',
'handleRequest',
'fr', // better way of handling this?
'en'
); );
/** static $casting = array(
* An array of files to ignore from the listing 'Version' => 'Text',
* 'Lang' => 'Text',
* @var array 'Module' => 'Text',
*/ 'LanguageTitle' => 'Text'
static $ignored_files = array('.', '..', '.DS_Store', '.svn', '.git', 'assets', 'themes'); );
/**
* An array of case insenstive values to use as readmes
*
* @var array
*/
static $readme_files = array('readme', 'readme.md', 'readme.txt', 'readme.markdown');
/** function init() {
* Main documentation page parent::init();
*/
function index() { $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN"));
return $this->customise(array(
'DocumentedModules' => $this->DocumentedModules() if(!$canAccess) return Security::permissionFailure($this);
))->renderWith(array('DocumentationViewer_index', 'DocumentationViewer'));
} }
/** /**
* Individual documentation page * Handle the url parsing for the documentation. In order to make this
* user friendly this does some tricky things..
* *
* @param HTTPRequest * 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
*/ */
function parse($request) { public function handleRequest(SS_HTTPRequest $request) {
require_once('../sapphiredocs/thirdparty/markdown.php');
$page = $request->param('Page'); $this->Version = $request->shift();
$module = $request->param('Module'); $this->Lang = $request->shift();
$path = BASE_PATH .'/'. $module .'/docs'; $this->Remaining = $request->shift(10);
if($content = $this->findPage($path, $page)) { DocumentationService::load_automatic_registration();
$title = $page;
$content = Markdown(file_get_contents($content)); if(isset($this->Version)) {
// check to see if its a valid version. If its not a float then its not actually a version
// its actually a language and it needs to change. So this means we support 2 structures
// /2.4/en/sapphire/page and
// /en/sapphire/page which is a link to the latest one
if(!is_numeric($this->Version)) {
// not numeric so /en/sapphire/folder/page
if(isset($this->Lang) && $this->Lang)
array_unshift($this->Remaining, $this->Lang);
$this->Lang = $this->Version;
$this->Version = null;
} }
else { else {
$title = 'Page not Found'; // if(!DocumentationService::is_registered_version($this->Version)) {
$content = false; // $this->httpError(404, 'The requested version could not be found.');
// }
}
}
if(isset($this->Lang)) {
// check to see if its a valid language
// if(!DocumentationService::is_registered_language($this->Lang)) {
// $this->httpError(404, 'The requested language could not be found.');
// }
}
else {
$this->Lang = 'en';
} }
return $this->customise(array( return parent::handleRequest($request);
'Title' => $title,
'Content' => $content
))->renderWith('DocumentationViewer');
} }
/** /**
* Returns an array of the modules installed. Currently to determine if a module is * Custom templates for each of the sections.
* installed look at all the folders and check is a _config file.
*
* @return array
*/ */
function getModules() { function getViewer($action) {
$modules = scandir(BASE_PATH); // 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($modules) { if($this->Remaining) {
foreach($modules as $key => $module) { $params = count(array_unique($this->Remaining));
if(!is_dir(BASE_PATH .'/'. $module) || in_array($module, self::$ignored_files, true) || !file_exists(BASE_PATH . '/'. $module .'/_config.php')) {
unset($modules[$key]); switch($params) {
case '1':
return parent::getViewer('home');
case '2':
return parent::getViewer('folder');
default:
if($module = $this->getModule()) {
$params = $this->Remaining;
array_shift($params);
$path = implode('/', array_unique($params));
} }
if(is_dir($module->getPath() . $path)) return parent::getViewer('folder');
} }
} }
return $modules; return parent::getViewer($action);
} }
/** /**
* Generate a set of modules for the home page * Return all the available languages. Optionally the languages which are
* available for a given module
* *
* @param String - The name of the module
* @return DataObjectSet * @return DataObjectSet
*/ */
function DocumentedModules() { function getLanguages($module = false) {
$output = new DataObjectSet();
$modules = new DataObjectSet(); if($module) {
// lookup the module for the available languages
// include sapphire first // @todo
$modules->push(new ArrayData(array( }
'Title' => 'sapphire', else {
'Content' => $this->generateNestedTree('sapphire'), $languages = DocumentationService::get_registered_languages();
'Readme' => $this->readmeExists('sapphire')
)));
$extra_ignore = array('sapphire'); if($languages) {
foreach($languages as $key => $lang) {
foreach($this->getModules() as $module) { if(stripos($_SERVER['REQUEST_URI'], '/'. $this->Lang .'/') === false) {
if(!in_array($module, $extra_ignore) && $this->moduleHasDocs($module)) { // no language is in the URL currently. It needs to insert the language
$modules->push(new ArrayData(array( // into the url like /sapphire/install to /en/sapphire/install
'Title' => $module, //
'Content' => $this->generateNestedTree($module), // @todo
'Readme' => $this->readmeExists($module) }
$link = str_ireplace('/'.$this->Lang .'/', '/'. $lang .'/', $_SERVER['REQUEST_URI']);
$output->push(new ArrayData(array(
'Title' => $lang,
'Link' => $link
))); )));
} }
} }
return $modules;
} }
/**
* Generate a list of modules (folder which has a _config) which have no /docs/ folder
*
* @return DataObjectSet
*/
function UndocumentedModules() {
$modules = $this->getModules();
$undocumented = array();
if($modules) {
foreach($modules as $module) {
if(!$this->moduleHasDocs($module)) $undocumented[] = $module;
}
}
return implode(', ', $undocumented);
}
/**
* Helper function to determine whether a module has documentation
*
* @param String - Module folder name
* @return bool - Has docs folder
*/
function moduleHasDocs($module) {
return is_dir(BASE_PATH .'/'. $module .'/docs/');
}
/**
* Work out if a module contains a readme
*
* @param String - Module to check
* @return bool|String - of path
*/
private function readmeExists($module) {
$children = scandir(BASE_PATH.'/'.$module);
$readmeOptions = self::$readme_files;
if($children) {
foreach($children as $i => $file) {
if(in_array(strtolower($file), $readmeOptions)) return $file;
}
}
return false;
}
/**
* Find a documentation page within a given module.
*
* @param String - Path to Module
* @param String - Name of doc page
*
* @return String|false - File path
*/
private function findPage($path, $name) {
// open docs folder
$handle = opendir($path);
if($handle) {
while (false !== ($file = readdir($handle))) {
$newpath = $path .'/'. $file;
if(!in_array($file, self::$ignored_files)) {
if(is_dir($newpath)) return $this->findPage($newpath, $name);
elseif(strtolower($this->formatStringForTitle($file)) == strtolower($name)) {
return $newpath;
}
}
}
}
return false;
}
/**
* Generate a nested tree for a given folder via recursion
*
* @param String - module to generate
*/
private function generateNestedTree($module) {
$path = BASE_PATH . '/'. $module .'/docs/';
return (is_dir($path)) ? $this->recursivelyGenerateTree($path, $module) : false;
}
/**
* Recursive method to generate the tree
*
* @param String - folder to work through
* @param String - module we're working through
*/
private function recursivelyGenerateTree($path, $module, $output = '') {
$output .= "<ul class='tree'>";
$handle = opendir($path);
if($handle) {
while (false !== ($file = readdir($handle))) {
if(!in_array($file, self::$ignored_files)) {
$newPath = $path.'/'.$file;
// if the file is a dir nest the pages
if(is_dir($newPath)) {
// if this has a number
$output .= "<li class='folder'>". $this->formatStringForTitle($file) ."</li>";
$output = $this->recursivelyGenerateTree($newPath, $module, $output);
}
else {
$offset = (strpos($file,'-') > 0) ? strpos($file,'-') + 1 : 0;
$file = substr(str_ireplace('.md', '', $file), $offset);
$output .= "<li class='page'><a href='". Director::absoluteBaseURL() . 'dev/docs/' . $module .'/'. $file . "'>". $this->formatStringForTitle($file) ."</a></li>";
}
}
}
}
closedir($handle);
$output .= "</ul>";
return $output; return $output;
} }
/** /**
* Take a file name and generate a 'nice' title for it. * Get all the versions loaded into the module. If the project is only displaying from
* the filesystem then they are loaded under the 'Current' namespace.
* *
* example. 01-Getting-Started -> Getting Started * @todo Only show 'core' versions (2.3, 2.4) versions of the modules are going
* to spam this
* *
* @param String - raw title * @param String $module name of module to limit it to eg sapphire
* @return String - nicely formatted one * @return DataObjectSet
*/ */
private function formatStringForTitle($title) { function getVersions($module = false) {
// remove numbers if used. $versions = DocumentationService::get_registered_versions($module);
if(substr($title, 2, 1) == '-') $title = substr($title, 3); $output = new DataObjectSet();
// change - to spaces foreach($versions as $key => $version) {
$title = str_ireplace('-', ' ', $title); // work out the link to this version of the documentation.
//
// @todo Keep the user on their given page rather than redirecting to module.
// @todo Get links working
$linkingMode = ($this->Version == $version) ? 'current' : 'link';
// remove extension if(!$version) $version = 'Current';
$title = str_ireplace(array('.md', '.markdown'), '', $title); $major = (in_array($version, DocumentationService::get_major_versions())) ? true : false;
return $title; $output->push(new ArrayData(array(
'Title' => $version,
'Link' => $_SERVER['REQUEST_URI'],
'LinkingMode' => $linkingMode,
'MajorRelease' => $major
)));
} }
return $output;
}
/**
* Generate the module which are to be documented. It filters
* the list based on the current head version. It displays the contents
* from the index.md file on the page to use.
*
* @return DataObject
*/
function getModules($version = false, $lang = false) {
if(!$version) $version = $this->Version;
if(!$lang) $lang = $this->Lang;
$modules = DocumentationService::get_registered_modules($version, $lang);
$output = new DataObjectSet();
if($modules) {
foreach($modules as $module) {
// build the dataset. Load the $Content from an index.md
$output->push(new ArrayData(array(
'Title' => $module->getTitle(),
'Code' => $module,
'Content' => DocumentationParser::parse($module->getPath(), array('index'))
)));
}
}
return $output;
}
/**
* Get the currently accessed entity from the site.
*
* @return false|DocumentationEntity
*/
function getModule() {
if($this->Remaining && is_array($this->Remaining)) {
return DocumentationService::is_registered_module($this->Remaining[0], $this->Version, $this->Lang);
}
return false;
}
/**
* Get the related pages to this module and the children to those pages
*
* @todo this only handles 2 levels. Could make it recursive
*
* @return false|DataObjectSet
*/
function getModulePages() {
if($module = $this->getModule()) {
$pages = DocumentationParser::get_pages_from_folder($module->getPath());
if($pages) {
foreach($pages as $page) {
$linkParts = array($module->getModuleFolder());
// don't include the 'index in the url
if($page->Title != "Index") $linkParts[] = $page->Filename;
$page->Link = $this->Link($linkParts);
$page->LinkingMode = 'link';
$page->Children = false;
if(isset($this->Remaining[1])) {
if(strtolower($this->Remaining[1]) == $page->Filename) {
$page->LinkingMode = 'current';
if(is_dir($page->Path)) {
$children = DocumentationParser::get_pages_from_folder($page->Path);
$segments = array($module->getModuleFolder(), $this->Remaining[1]);
foreach($children as $child) {
$child->Link = $this->Link(array_merge($segments, array($child->Filename)));
}
$page->Children = $children;
}
}
}
}
}
return $pages;
}
return false;
}
/**
* 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
*/
function getContent() {
if($module = $this->getModule()) {
// name of the module. Throw it away since we already have the module path.
$filepath = $this->Remaining;
array_shift($filepath);
return DocumentationParser::parse($module->getPath(), $filepath);
}
return false;
}
/**
* Generate a list of breadcrumbs for the user. Based off the remaining params
* in the url
*
* @return DataObjectSet
*/
function getBreadcrumbs() {
$pages = $this->Remaining;
$output = new DataObjectSet();
$output->push(new ArrayData(array(
'Title' => ($this->Version) ? $this->Version : _t('DocumentationViewer.DOCUMENTATION', 'Documentation'),
'Link' => $this->Link()
)));
if($pages) {
$path = array();
foreach($pages as $page => $title) {
if($title) {
$path[] = $title;
$output->push(new ArrayData(array(
'Title' => DocumentationParser::clean_page_name($title),
'Link' => $this->Link($path)
)));
}
}
}
return $output;
}
/**
* Return the base link to this documentation location
*
* @todo Make this work on non /dev/
* @return String
*/
public function Link($path = false) {
$base = Director::absoluteBaseURL();
// @todo
$loc = 'dev/docs/';
$version = ($this->Version) ? $this->Version . '/' : false;
$lang = ($this->Lang) ? $this->Lang .'/' : false;
$action = '';
if(is_string($path)) $action = $path . '/';
if(is_array($path)) {
foreach($path as $key => $value) {
if($value) {
$action .= $value .'/';
}
}
}
return $base . $loc . $version . $lang . $action;
}
/**
* Build the language dropdown.
*
* @todo do this on a page by page rather than global
*
* @return Form
*/
function LanguageForm() {
if($module = $this->getModule()) {
$langs = DocumentationService::get_registered_languages($module->getModuleFolder());
}
else {
$langs = DocumentationService::get_registered_languages();
}
$fields = new FieldSet(
$dropdown = new DropdownField(
'LangCode',
_t('DocumentationViewer.LANGUAGE', 'Language'),
$langs,
$this->Lang
)
);
$actions = new FieldSet(
new FormAction('doLanguageForm', _t('DocumentationViewer.CHANGE', 'Change'))
);
$dropdown->setDisabled(true);
return new Form($this, 'LanguageForm', $fields, $actions);
}
/**
* Process the language change
*
*/
function doLanguageForm($data, $form) {
$this->Lang = (isset($data['LangCode'])) ? $data['LangCode'] : 'en';
return $this->redirect($this->Link());
}
} }

View File

@ -1,83 +1,84 @@
/** /**
* Documentation Viewer Styles. * Documentation Viewer Styles.
* *
* @author Will Rossiter <will@silverstripe.com>
*/ */
/* Reset */
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{ margin:0;padding: 0;} body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{ margin:0;padding: 0;}
/* Core */
html { background: #f4f4f4;} html { background: #f4f4f4;}
body { font: 14px/1.1 Arial,sans-serif; color: #444; } body { font: 14px/1.1 Arial,sans-serif; color: #444; }
a { color: #1389ce; text-decoration: none; } a { color: #1389ce; text-decoration: none; }
a:hover { text-decoration: underline;} a:hover,
p { a:focus { text-decoration: underline;}
font-size: 14px;
line-height: 22px;
margin-bottom: 22px;
}
ul { margin: 8px 16px 20px 20px; } p { font-size: 14px; line-height: 22px; margin-bottom: 22px; }
li { font-size: 12px; line-height: 18px; margin-bottom: 8px;}
ul { margin: 11px 0 22px 20px; }
li { font-size: 12px; line-height: 13px; margin-bottom: 8px;}
h1 { font-size: 30px; margin-bottom: 18px; color: #111; } h1 { font-size: 33px; line-height: 33px; margin-bottom: 22px; color: #111; letter-spacing: -1px;}
h2 { font-size: 24px; margin-bottom: 16px; color: #111; } h2 { font-size: 24px; line-height: 33px; margin-bottom: 11px; color: #111; }
h3 { font-size: 18px; margin-bottom: 16px; color: #111; } h3 { font-size: 18px; line-height: 22px; margin-bottom: 11px; color: #111; }
h4 { font-size: 16px; margin-bottom: 6px; line-height: 16px;} h4 { font-size: 16px; margin-bottom: 11px; line-height: 22px;}
h5 { font-size: 14px; line-height: 18px; margin-bottom: 6px;} h5 { font-size: 14px; line-height: 22px; margin-bottom: 11px;}
pre { pre {
margin-bottom: 18px; margin-bottom: 22px;
font-family:'Bitstream Vera Sans Mono',Monaco, 'Courier New', monospace; font-family:'Bitstream Vera Sans Mono',Monaco, 'Courier New', monospace;
border-left: 4px solid #eee;
background: #f4f4f4; background: #f4f4f4;
padding: 12px; padding: 11px;
font-size: 11px;
} }
#container { width: 960px; margin: 20px auto; padding: 20px; background: #fff; overflow: hidden; } /* Forms */
fieldset { border: none; }
#header { border-bottom: 3px solid #535360; padding-top: 10px; margin-bottom: 30px; } /* Container */
#header h1 { margin-bottom: 9px;} #container { width: 960px; margin: 44px auto 22px auto; padding: 22px 30px; background: #fff; overflow: hidden;
#header h1 a { text-decoration: none; font-size: 30px; color: #333; letter-spacing: -1px;} -webkit-box-shadow: 0 0 20px #ccc; -moz-box-shadow: 0 0 20px #ccc;}
#header .breadcrumbs { font-size: 12px; } /* Header */
#left-column { #header { padding: 11px 0 0 0; margin-bottom: 22px; }
width: 640px; #header h1 { margin-bottom: 0px; line-height: 33px;}
float: left; #header h1 a { text-decoration: none; font-size: 30px; color: #121929; letter-spacing: -1px;}
}
#header #breadcrumbs p { font-size: 11px; margin: 0 0 10px 0; color: #798D85;}
#header #breadcrumbs p a { color: #798D85;}
/* Language Bar */
#language { position: absolute; top: 12px; left: 50%; margin-left: -480px; width: 960px; }
#language label { float: left; width: 830px; line-height: 19px; text-align: right; font-size: 11px; color: #999;}
#language select { float: right; width: 120px;}
#language input.action { float: right; margin-top: 4px;}
/* Footer */
#footer { width: 960px; margin: 22px auto; }
#footer p { font-size: 11px; line-height: 11px; color: #798D85;}
#footer p a { color: #798D85;}
/* Content */
#layout { }
#content { }
/* Versions */
#versions-nav { background: #121929; margin: 0 0 44px; padding: 10px 0 0 10px; overflow: hidden;}
#versions-nav h2 { font-size: 11px; color: #fff; font-weight: normal; float: left; margin-right: 5px;}
#versions-nav ul { margin: 0; padding: 0; float: left;}
#versions-nav li { list-style: none; }
#versions-nav li a { display: block; float: left; margin-left: 4px; padding: 11px; font-size: 14px;}
#versions-nav li a.current { background: #fff;}
#left-column { width: 640px; float: left; }
#right-column { #right-column {
width: 260px; width: 260px;
float: right; float: right;
} }
#home #left-column { width: 500px; }
#home #right-column { width: 340px; }
#home .box {
margin: 0 12px 12px 0px;
border: 1px solid #d8d8d8;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
}
#home .box h2 {
background: #535360;
border: 1px solid #535360;
-moz-border-top-radius: 4px;
-webkit-border-top-left-radius: 4px;
-webkit-border-top-right-radius: 4px;
padding: 6px 8px;
font-weight: 500;
color: #fff;
font-size: 13px;
}
#home .box h2 a {
background: url(../images/readme.png) no-repeat right center;
padding: 2px 20px 0 0;
font-size: 11px;
color: #fff;
display: block;
float: right;
}
#right-column .box { #right-column .box {
margin: 0 12px 12px 0; margin: 0 12px 12px 0;
} }
@ -103,19 +104,27 @@ pre {
font-style: italic; font-style: italic;
} }
.module { margin: 10px -; }
/** /**
* TOC * TOC
*/ */
ul#toc { .sidebar-box {
margin: 0; margin: 0 0 11px 0;
padding: 20px; padding: 11px 15px;
background: #f4f4f4; background: #f4f4f4;
width: 220px; width: 220px;
} }
ul#toc h4 { font-size: 12px; margin-bottom: 0px;} .sidebar-box ul { margin: 0; padding: 0;}
ul#toc li { list-style: none; margin: 0 0 4px 0; } .sidebar-box h4 { font-size: 12px; margin-bottom: 11px;}
ul#toc li.h1 { margin-top: 10px; font-weight: bold;} .sidebar-box ul li { list-style: none; }
ul#toc li.h2 { margin: 0 0 0px 10px; font-size: 11px;} .sidebar-box ul li .current { font-weight: bold;}
ul#toc li.h3 { margin-left: 20px; font-size: 10px; } .sidebar-box ul li.h1 { margin-top: 11px; font-weight: bold;}
ul#toc li.h4 { margin-right: 30px; font-size: 10px; } .sidebar-box ul li.h2,
.sidebar-box ul ul { margin-top: 8px;}
.sidebar-box ul li li { font-size: 11px; margin-left: 10px;}
.sidebar-box ul li.h3,
.sidebar-box ul li li li { margin-left: 20px; font-size: 10px; margin-left: 20px;}
.sidebar-box ul li.h4,
.sidebar-box ul li li li li { margin-right: 30px; font-size: 10px; margin-left: 20px; }

View File

@ -1,28 +0,0 @@
# Writing Documentation #
Your documentation needs to go in the specific modules doc folder which it refers mostly too. For example if you want to document
a feature of your custom module 'MyModule' you need to create markdown files in mymodule/doc/.
The files have to end with the __.md__ extension. The documentation viewer will automatically replace hyphens (-) with spaces (since you cannot
have spaces easily in some file systems).
## Syntax ##
This uses a customized markdown extra parser. To view the syntax for page formatting check out [http://daringfireball.net/projects/markdown/syntax][Daring Fireball]
## Creating Hierarchy ##
The document viewer supports folder structure. There is no limit on depth or number of sub categories you can create.
## Customizing Page Order ##
Sometimes you will have pages which you want at the top of the documentation viewer summary. Pages like Getting-Started will come after Advanced-Usage
due to the default alphabetical ordering.
To handle this you can use a number prefix for example __01-My-First-Folder__ which would be the first folder in the list.
DocumentationViewer will remove the __01-__ from the name as well so you don't need to worry about labels for your folders with numbers. It will be
outputted in the front end as __My First Folder__

View File

@ -0,0 +1,22 @@
# Helpful Configuration Options
DocumentationService::set_ignored_files(array());
If you want to ignore (hide) certain file types from being included.
DocumentationService::set_automatic_registration(false);
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
with the above.
DocumentationService::register($module, $path, $version = 'current', $lang = 'en', $major_release = false)
Registers a module to be included in the system (if automatic registration is off or you need
to load a module outside a documentation path).
DocumentationService::unregister($module, $version = false, $lang = false)
Unregister a module (removes from documentation list). You can specify the module, the version
and the lang. If no version is specified then all folders of that lang are removed. If you do
not specify a version or lang the whole module will be removed from the documentation.

View File

@ -0,0 +1,32 @@
# Writing Documentation #
Your documentation needs to go in the specific modules docs folder which it refers mostly too. For example if you want to document
a feature of your custom module 'MyModule' you need to create markdown files in mymodule/docs/.
The files have to end with the __.md__ extension. The documentation viewer will automatically replace hyphens (-) with spaces (since you cannot
have spaces web / file systems).
Also docs folder should be localized. Even if you do not plan on using multiple languages you should at least write your documentation
in a 'en' subfolder
/module/docs/en/
## Syntax ##
This uses a customized markdown extra parser. To view the syntax for page formatting check out [http://daringfireball.net/projects/markdown/syntax][Daring Fireball]
## Creating Hierarchy ##
The document viewer supports folder structure. There is a 9 folder limit on depth / number of sub categories you can create.
Each level deep it will generate the nested urls.
## Directory Listing ##
Each folder you create should also contain a __index.md__ file (see sapphiredocs/doc/en/index.md) which contains an overview of the
module and related links.
## Table of Contents ##
The table of contents on each module page is generated

11
docs/en/index.md Normal file
View File

@ -0,0 +1,11 @@
### Sapphire Documentation Module
This module has been developed to read and display content from markdown files in webbrowser. It is an easy
way to bundle end user documentation within a SilverStripe installation.
See <a href="dev/docs/en/sapphiredocs/writing-documentation">Writing Documentation</a> for more information on how to write markdown files which
are available here.
To include your docs file here create a __docs/en/index.md__ file. You can also include custom paths and versions. To configure the documentation system the configuration information is available on the <a href="dev/docs/en/sapphiredocs/configuration-options">Configurations</a>
page.

View File

@ -21,5 +21,16 @@
$('#table-of-contents').prepend(toc); $('#table-of-contents').prepend(toc);
} }
/** ---------------------------------------------
* LANGAUGE SELECTER
*
* Hide the change button and do it onclick
*/
$("#Form_LanguageForm .Actions").hide();
$("#Form_LanguageForm select").change(function() {
$("#Form_LanguageForm").submit();
});
}); });
})(jQuery); })(jQuery);

View File

@ -15,12 +15,39 @@
<body> <body>
<div id="container"> <div id="container">
<div id="header"> <div id="header">
<h1><a href="dev/docs/">SilverStripe Documentation</a></h1> <h1><a href="$Link"><% _t('SILVERSTRIPEDOCUMENTATION', 'SilverStripe Documentation') %></a></h1>
$Breadcrumbs <div id="language">
$LanguageForm
</div> </div>
<div id="breadcrumbs">
<% include DocBreadcrumbs %>
</div>
</div>
<div id="layout">
<div id="versions-nav">
<h2>Versions:</h2>
<ul>
<% control Versions %>
<% if MajorRelease %>
<li class="major-release"><a href="$Link" class="$LinkingMode">$Title</a></li>
<% else %>
<li class="module-only"><a href="$Link" class="$LinkingMode">$Title</a></li>
<% end_if %>
<% end_control %>
</ul>
</div>
<div id="content">
$Layout $Layout
</div> </div>
</div>
</div>
<div id="footer">
<p>Documentation powered by <a href="http://www.silverstripe.org">SilverStripe</a>. Found a typo? <a href="http://github.com/chillu/silverstripe-doc-restructuring">Contribute to the Documentation Project</a>.</p>
</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,5 @@
<p>
<% control Breadcrumbs %>
<a href="$Link">$Title</a> <% if Last %><% else %>&rsaquo;<% end_if %>
<% end_control %>
</p>

View File

@ -0,0 +1,18 @@
<div id="in-this-module" class="sidebar-box">
<h4>In this module</h4>
<ul>
<% control ModulePages %>
<li>
<a href="$Link" class="$LinkingMode">$Title</a>
<% if Children %>
<ul>
<% control Children %>
<li><a href="$Link" class="$LinkingMode">$Title</a></li>
<% end_control %>
</ul>
<% end_if %>
</li>
<% end_control %>
</ul>
</div>

View File

@ -0,0 +1 @@
<div id="table-of-contents" class="sidebar-box"></div>

View File

@ -1,11 +1,14 @@
<div id="left-column"> <div id="documentation-page">
<div id="left-column">
<% if Content %> <% if Content %>
$Content $Content
<% else %> <% else %>
<p>Woops no documentation for this page</p> <p>Woops page not found</p>
<% end_if %> <% end_if %>
</div> </div>
<div id="right-column"> <div id="right-column">
<div id="table-of-contents"></div> <% include DocTableOfContents %>
<% include DocInThisModule %>
</div>
</div> </div>

View File

@ -0,0 +1,15 @@
<div id="module-home">
<div id="left-column">
<% if Content %>
$Content
<% else %>
frs
<h2>$Title</h2>
<% end_if %>
</div>
<div id="right-column">
<% include DocInThisModule %>
</div>
</div>

View File

@ -0,0 +1,10 @@
<div id="home">
<% control Modules %>
<% if Content %>
<div class="module">
$Content
</div>
<% end_if %>
<% end_control %>
</div>

View File

@ -1,29 +0,0 @@
<div id="home">
<% control DocumentedModules %>
<% if First %>
<div id="left-column">
<div class="box">
<h2>$Title $Readme</h2>
$Content
</div>
</div>
<div id="right-column">
<% else %>
<div class="box">
<h2>$Title $Readme</h2>
$Content
</div>
<% end_if %>
<% end_control %>
</div>
<% if UndocumentedModules %>
<div class='undocumented-modules'>
<p>Undocumented Modules: $UndocumentedModules</p>
</div>
<% end_if %>
</div>

11
tests/DocumentTests.yml Normal file
View File

@ -0,0 +1,11 @@
Permission:
admin:
Code: ADMIN
Group:
admins:
Code: admins
Permissions: =>Permission.admin
Member:
admin:
Email: admin@test.com
Groups: =>Group.admins

View File

@ -0,0 +1,52 @@
<?php
/**
* Some of these tests are simply checking that pages load. They should not assume
* somethings working.
*
* @package sapphiredocs
*/
class DocumentationViewerTests extends FunctionalTest {
static $fixture_file = 'sapphiredocs/tests/DocumentTests.yml';
function testCleanPageNames() {
$names = array(
'documentation-Page',
'documentation_Page',
'documentation.md',
'documentation.pdf',
'documentation.file.txt',
'.hidden'
);
$should = array(
'Documentation Page',
'Documentation Page',
'Documentation',
'Documentation',
'Documentation',
'.hidden' // don't display something without a title
);
foreach($names as $key => $value) {
$this->assertEquals(DocumentationParser::clean_page_name($value), $should[$key]);
}
}
function testDocumentationEntityAccessing() {
$entity = new DocumentationEntity('docs', '1.0', '../sapphiredocs/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->getModuleFolder(), 'docs');
$this->assertTrue($entity->hasVersion('1.0'));
$this->assertFalse($entity->hasVersion('2.0'));
$this->assertTrue($entity->hasLanguage('en'));
$this->assertFalse($entity->hasLanguage('fr'));
}
}

5
tests/docs-2/en/index.md Normal file
View File

@ -0,0 +1,5 @@
## english test
index
2.0

5
tests/docs/de/index.md Normal file
View File

@ -0,0 +1,5 @@
## german test
index
1.0

5
tests/docs/de/test.md Normal file
View File

@ -0,0 +1,5 @@
## german test
test
1.0

5
tests/docs/en/index.md Normal file
View File

@ -0,0 +1,5 @@
## english test
index
1.0

5
tests/docs/en/test.md Normal file
View File

@ -0,0 +1,5 @@
## english test
test
1.0