diff --git a/code/DocumentationEntity.php b/code/DocumentationEntity.php new file mode 100644 index 0000000..feff75b --- /dev/null +++ b/code/DocumentationEntity.php @@ -0,0 +1,182 @@ + '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 .'/'; + } +} \ No newline at end of file diff --git a/code/DocumentationParser.php b/code/DocumentationParser.php new file mode 100644 index 0000000..d3af3d9 --- /dev/null +++ b/code/DocumentationParser.php @@ -0,0 +1,153 @@ + 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; + } +} \ No newline at end of file diff --git a/code/DocumentationService.php b/code/DocumentationService.php new file mode 100644 index 0000000..a72f5aa --- /dev/null +++ b/code/DocumentationService.php @@ -0,0 +1,342 @@ + '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; + } +} \ No newline at end of file diff --git a/code/DocumentationViewer.php b/code/DocumentationViewer.php index 75c3bd0..43156a3 100755 --- a/code/DocumentationViewer.php +++ b/code/DocumentationViewer.php @@ -3,277 +3,420 @@ /** * Documentation Viewer. * - * Reads the bundled markdown files from docs/ folders and displays output in a formatted page at /dev/docs/. - * For more documentation on how to use this class see the documentation online in /dev/docs/ or in the - * /sapphiredocs/docs folder + * Reads the bundled markdown files from documentation folders and displays the output (either + * via markdown or plain text) + * + * For more documentation on how to use this class see the documentation in /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 * @package sapphiredocs */ class DocumentationViewer extends Controller { - - static $url_handlers = array( - '' => 'index', - '$Module/$Page/$OtherPage' => 'parse' + + static $allowed_actions = array( + 'LanguageForm', + 'doLanguageForm', + 'handleRequest', + 'fr', // better way of handling this? + 'en' ); - /** - * An array of files to ignore from the listing - * - * @var array - */ - static $ignored_files = array('.', '..', '.DS_Store', '.svn', '.git', 'assets', 'themes'); + static $casting = array( + 'Version' => 'Text', + 'Lang' => 'Text', + 'Module' => 'Text', + 'LanguageTitle' => 'Text' + ); - /** - * 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() { + parent::init(); + + $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN")); - - /** - * Main documentation page - */ - function index() { - return $this->customise(array( - 'DocumentedModules' => $this->DocumentedModules() - ))->renderWith(array('DocumentationViewer_index', 'DocumentationViewer')); + if(!$canAccess) return Security::permissionFailure($this); } /** - * 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) { - require_once('../sapphiredocs/thirdparty/markdown.php'); + public function handleRequest(SS_HTTPRequest $request) { + + $this->Version = $request->shift(); + $this->Lang = $request->shift(); + + $this->Remaining = $request->shift(10); + + DocumentationService::load_automatic_registration(); + + 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 - $page = $request->param('Page'); - $module = $request->param('Module'); - - $path = BASE_PATH .'/'. $module .'/docs'; - - if($content = $this->findPage($path, $page)) { - $title = $page; - $content = Markdown(file_get_contents($content)); + 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 { + // if(!DocumentationService::is_registered_version($this->Version)) { + // $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 { - $title = 'Page not Found'; - $content = false; + $this->Lang = 'en'; } - return $this->customise(array( - 'Title' => $title, - 'Content' => $content - ))->renderWith('DocumentationViewer'); + return parent::handleRequest($request); } /** - * Returns an array of the modules installed. Currently to determine if a module is - * installed look at all the folders and check is a _config file. - * - * @return array + * Custom templates for each of the sections. */ - function getModules() { - $modules = scandir(BASE_PATH); + 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($modules) { - foreach($modules as $key => $module) { - if(!is_dir(BASE_PATH .'/'. $module) || in_array($module, self::$ignored_files, true) || !file_exists(BASE_PATH . '/'. $module .'/_config.php')) { - unset($modules[$key]); - } + if($this->Remaining) { + $params = count(array_unique($this->Remaining)); + + 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 */ - function DocumentedModules() { + function getLanguages($module = false) { + $output = new DataObjectSet(); - $modules = new DataObjectSet(); - - // include sapphire first - $modules->push(new ArrayData(array( - 'Title' => 'sapphire', - 'Content' => $this->generateNestedTree('sapphire'), - 'Readme' => $this->readmeExists('sapphire') - ))); - - $extra_ignore = array('sapphire'); - - foreach($this->getModules() as $module) { - if(!in_array($module, $extra_ignore) && $this->moduleHasDocs($module)) { - $modules->push(new ArrayData(array( - 'Title' => $module, - 'Content' => $this->generateNestedTree($module), - 'Readme' => $this->readmeExists($module) - ))); + if($module) { + // lookup the module for the available languages + + // @todo + } + else { + $languages = DocumentationService::get_registered_languages(); + + if($languages) { + foreach($languages as $key => $lang) { + + if(stripos($_SERVER['REQUEST_URI'], '/'. $this->Lang .'/') === false) { + // no language is in the URL currently. It needs to insert the language + // into the url like /sapphire/install to /en/sapphire/install + // + // @todo + } + + $link = str_ireplace('/'.$this->Lang .'/', '/'. $lang .'/', $_SERVER['REQUEST_URI']); + + $output->push(new ArrayData(array( + 'Title' => $lang, + 'Link' => $link + ))); + } } } - - return $modules; + + return $output; } - + /** - * Generate a list of modules (folder which has a _config) which have no /docs/ folder + * 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. * + * @todo Only show 'core' versions (2.3, 2.4) versions of the modules are going + * to spam this + * + * @param String $module name of module to limit it to eg sapphire * @return DataObjectSet */ - function UndocumentedModules() { - $modules = $this->getModules(); - $undocumented = array(); + function getVersions($module = false) { + $versions = DocumentationService::get_registered_versions($module); + $output = new DataObjectSet(); - if($modules) { - foreach($modules as $module) { - if(!$this->moduleHasDocs($module)) $undocumented[] = $module; - } + foreach($versions as $key => $version) { + // 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'; + + if(!$version) $version = 'Current'; + $major = (in_array($version, DocumentationService::get_major_versions())) ? true : false; + + $output->push(new ArrayData(array( + 'Title' => $version, + 'Link' => $_SERVER['REQUEST_URI'], + 'LinkingMode' => $linkingMode, + 'MajorRelease' => $major + ))); } - 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 .= ""; - return $output; } /** - * Take a file name and generate a 'nice' title for it. + * 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. * - * example. 01-Getting-Started -> Getting Started - * - * @param String - raw title - * @return String - nicely formatted one - */ - private function formatStringForTitle($title) { - // remove numbers if used. - if(substr($title, 2, 1) == '-') $title = substr($title, 3); + * @return DataObject + */ + function getModules($version = false, $lang = false) { + if(!$version) $version = $this->Version; + if(!$lang) $lang = $this->Lang; - // change - to spaces - $title = str_ireplace('-', ' ', $title); + $modules = DocumentationService::get_registered_modules($version, $lang); + $output = new DataObjectSet(); - // remove extension - $title = str_ireplace(array('.md', '.markdown'), '', $title); + 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 $title; + 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()); + } } \ No newline at end of file diff --git a/css/DocumentationViewer.css b/css/DocumentationViewer.css index 5fed7d7..7096439 100644 --- a/css/DocumentationViewer.css +++ b/css/DocumentationViewer.css @@ -1,83 +1,84 @@ /** * Documentation Viewer Styles. * + * @author Will Rossiter */ +/* 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;} +/* Core */ html { background: #f4f4f4;} body { font: 14px/1.1 Arial,sans-serif; color: #444; } a { color: #1389ce; text-decoration: none; } - a:hover { text-decoration: underline;} -p { - font-size: 14px; - line-height: 22px; - margin-bottom: 22px; -} + a:hover, + a:focus { text-decoration: underline;} + +p { font-size: 14px; line-height: 22px; margin-bottom: 22px; } -ul { margin: 8px 16px 20px 20px; } - 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; } -h2 { font-size: 24px; margin-bottom: 16px; color: #111; } -h3 { font-size: 18px; margin-bottom: 16px; color: #111; } -h4 { font-size: 16px; margin-bottom: 6px; line-height: 16px;} -h5 { font-size: 14px; line-height: 18px; margin-bottom: 6px;} +h1 { font-size: 33px; line-height: 33px; margin-bottom: 22px; color: #111; letter-spacing: -1px;} +h2 { font-size: 24px; line-height: 33px; margin-bottom: 11px; color: #111; } +h3 { font-size: 18px; line-height: 22px; margin-bottom: 11px; color: #111; } +h4 { font-size: 16px; margin-bottom: 11px; line-height: 22px;} +h5 { font-size: 14px; line-height: 22px; margin-bottom: 11px;} pre { - margin-bottom: 18px; + margin-bottom: 22px; font-family:'Bitstream Vera Sans Mono',Monaco, 'Courier New', monospace; - border-left: 4px solid #eee; 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; } + +/* Container */ +#container { width: 960px; margin: 44px auto 22px auto; padding: 22px 30px; background: #fff; overflow: hidden; + -webkit-box-shadow: 0 0 20px #ccc; -moz-box-shadow: 0 0 20px #ccc;} -#header { border-bottom: 3px solid #535360; padding-top: 10px; margin-bottom: 30px; } - #header h1 { margin-bottom: 9px;} - #header h1 a { text-decoration: none; font-size: 30px; color: #333; letter-spacing: -1px;} +/* Header */ +#header { padding: 11px 0 0 0; margin-bottom: 22px; } + #header h1 { margin-bottom: 0px; line-height: 33px;} + #header h1 a { text-decoration: none; font-size: 30px; color: #121929; letter-spacing: -1px;} - #header .breadcrumbs { font-size: 12px; } -#left-column { - width: 640px; - float: left; -} + #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 { width: 260px; 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 { margin: 0 12px 12px 0; } @@ -103,19 +104,27 @@ pre { font-style: italic; } +.module { margin: 10px -; } + /** * TOC */ -ul#toc { - margin: 0; - padding: 20px; +.sidebar-box { + margin: 0 0 11px 0; + padding: 11px 15px; background: #f4f4f4; width: 220px; } - ul#toc h4 { font-size: 12px; margin-bottom: 0px;} - ul#toc li { list-style: none; margin: 0 0 4px 0; } - ul#toc li.h1 { margin-top: 10px; font-weight: bold;} - ul#toc li.h2 { margin: 0 0 0px 10px; font-size: 11px;} - ul#toc li.h3 { margin-left: 20px; font-size: 10px; } - ul#toc li.h4 { margin-right: 30px; font-size: 10px; } \ No newline at end of file + .sidebar-box ul { margin: 0; padding: 0;} + .sidebar-box h4 { font-size: 12px; margin-bottom: 11px;} + .sidebar-box ul li { list-style: none; } + .sidebar-box ul li .current { font-weight: bold;} + .sidebar-box ul li.h1 { margin-top: 11px; font-weight: bold;} + .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; } \ No newline at end of file diff --git a/docs/Writing-Documentation.md b/docs/Writing-Documentation.md deleted file mode 100644 index 5e0a8a8..0000000 --- a/docs/Writing-Documentation.md +++ /dev/null @@ -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__ - - \ No newline at end of file diff --git a/docs/en/Configuration-Options.md b/docs/en/Configuration-Options.md new file mode 100644 index 0000000..16a5be2 --- /dev/null +++ b/docs/en/Configuration-Options.md @@ -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. \ No newline at end of file diff --git a/docs/en/Writing-Documentation.md b/docs/en/Writing-Documentation.md new file mode 100644 index 0000000..db444fa --- /dev/null +++ b/docs/en/Writing-Documentation.md @@ -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 + + \ No newline at end of file diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..285ceb5 --- /dev/null +++ b/docs/en/index.md @@ -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 Writing Documentation 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 Configurations +page. + diff --git a/javascript/DocumentationViewer.js b/javascript/DocumentationViewer.js index 4d834c9..49d5398 100644 --- a/javascript/DocumentationViewer.js +++ b/javascript/DocumentationViewer.js @@ -21,5 +21,16 @@ $('#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); diff --git a/templates/DocumentationViewer.ss b/templates/DocumentationViewer.ss index 61b47f7..2d3dc49 100644 --- a/templates/DocumentationViewer.ss +++ b/templates/DocumentationViewer.ss @@ -15,12 +15,39 @@
+

<% _t('SILVERSTRIPEDOCUMENTATION', 'SilverStripe Documentation') %>

- $Layout +
+ $LanguageForm +
+ + +
+ +
+
+

Versions:

+ +
    + <% control Versions %> + <% if MajorRelease %> +
  • $Title
  • + <% else %> +
  • $Title
  • + <% end_if %> + <% end_control %> +
+
+
+ $Layout +
+
+ + + - + \ No newline at end of file diff --git a/templates/Includes/DocBreadcrumbs.ss b/templates/Includes/DocBreadcrumbs.ss new file mode 100644 index 0000000..563fed7 --- /dev/null +++ b/templates/Includes/DocBreadcrumbs.ss @@ -0,0 +1,5 @@ +

+ <% control Breadcrumbs %> + $Title <% if Last %><% else %>›<% end_if %> + <% end_control %> +

\ No newline at end of file diff --git a/templates/Includes/DocInThisModule.ss b/templates/Includes/DocInThisModule.ss new file mode 100644 index 0000000..9ef3578 --- /dev/null +++ b/templates/Includes/DocInThisModule.ss @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/templates/Includes/DocTableOfContents.ss b/templates/Includes/DocTableOfContents.ss new file mode 100644 index 0000000..9e90d8a --- /dev/null +++ b/templates/Includes/DocTableOfContents.ss @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/Layout/DocumentationViewer.ss b/templates/Layout/DocumentationViewer.ss index 46050b4..20da7f0 100644 --- a/templates/Layout/DocumentationViewer.ss +++ b/templates/Layout/DocumentationViewer.ss @@ -1,11 +1,14 @@ -
- <% if Content %> - $Content - <% else %> -

Woops no documentation for this page

- <% end_if %> -
+
+
+ <% if Content %> + $Content + <% else %> +

Woops page not found

+ <% end_if %> +
-
-
+
+ <% include DocTableOfContents %> + <% include DocInThisModule %> +
\ No newline at end of file diff --git a/templates/Layout/DocumentationViewer_folder.ss b/templates/Layout/DocumentationViewer_folder.ss new file mode 100644 index 0000000..511530d --- /dev/null +++ b/templates/Layout/DocumentationViewer_folder.ss @@ -0,0 +1,15 @@ +
+ +
+ <% if Content %> + $Content + <% else %> + frs +

$Title

+ <% end_if %> +
+ +
+ <% include DocInThisModule %> +
+
\ No newline at end of file diff --git a/templates/Layout/DocumentationViewer_home.ss b/templates/Layout/DocumentationViewer_home.ss new file mode 100755 index 0000000..f12a834 --- /dev/null +++ b/templates/Layout/DocumentationViewer_home.ss @@ -0,0 +1,10 @@ +
+ <% control Modules %> + <% if Content %> +
+ + $Content +
+ <% end_if %> + <% end_control %> +
\ No newline at end of file diff --git a/templates/Layout/DocumentationViewer_index.ss b/templates/Layout/DocumentationViewer_index.ss deleted file mode 100644 index 71e855a..0000000 --- a/templates/Layout/DocumentationViewer_index.ss +++ /dev/null @@ -1,29 +0,0 @@ -
- <% control DocumentedModules %> - <% if First %> -
-
-

$Title $Readme

- - $Content -
-
- -
- <% else %> -
-

$Title $Readme

- - $Content -
- <% end_if %> - - <% end_control %> -
- - <% if UndocumentedModules %> -
-

Undocumented Modules: $UndocumentedModules

-
- <% end_if %> -
\ No newline at end of file diff --git a/tests/DocumentTests.yml b/tests/DocumentTests.yml new file mode 100644 index 0000000..4ad6b98 --- /dev/null +++ b/tests/DocumentTests.yml @@ -0,0 +1,11 @@ +Permission: + admin: + Code: ADMIN +Group: + admins: + Code: admins + Permissions: =>Permission.admin +Member: + admin: + Email: admin@test.com + Groups: =>Group.admins \ No newline at end of file diff --git a/tests/DocumentationViewerTests.php b/tests/DocumentationViewerTests.php new file mode 100644 index 0000000..79468ff --- /dev/null +++ b/tests/DocumentationViewerTests.php @@ -0,0 +1,52 @@ + $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')); + } +} \ No newline at end of file diff --git a/tests/docs-2/en/index.md b/tests/docs-2/en/index.md new file mode 100644 index 0000000..fb47d89 --- /dev/null +++ b/tests/docs-2/en/index.md @@ -0,0 +1,5 @@ +## english test + +index + +2.0 \ No newline at end of file diff --git a/tests/docs/de/index.md b/tests/docs/de/index.md new file mode 100644 index 0000000..428720b --- /dev/null +++ b/tests/docs/de/index.md @@ -0,0 +1,5 @@ +## german test + +index + +1.0 \ No newline at end of file diff --git a/tests/docs/de/test.md b/tests/docs/de/test.md new file mode 100644 index 0000000..d66d88b --- /dev/null +++ b/tests/docs/de/test.md @@ -0,0 +1,5 @@ +## german test + +test + +1.0 \ No newline at end of file diff --git a/tests/docs/en/index.md b/tests/docs/en/index.md new file mode 100644 index 0000000..5f74132 --- /dev/null +++ b/tests/docs/en/index.md @@ -0,0 +1,5 @@ +## english test + +index + +1.0 \ No newline at end of file diff --git a/tests/docs/en/test.md b/tests/docs/en/test.md new file mode 100644 index 0000000..9769498 --- /dev/null +++ b/tests/docs/en/test.md @@ -0,0 +1,5 @@ +## english test + +test + +1.0 \ No newline at end of file