diff --git a/code/DocumentationParser.php b/code/DocumentationParser.php index 1ae3291..b897277 100755 --- a/code/DocumentationParser.php +++ b/code/DocumentationParser.php @@ -270,6 +270,9 @@ class DocumentationParser { $relativePath = dirname($page->getRelativePath()); if($relativePath == '.') $relativePath = ''; + // file base link + $fileBaseLink = Director::makeRelative(dirname($page->getPath())); + if($matches) { foreach($matches[0] as $i => $match) { $title = $matches[2][$i]; @@ -282,15 +285,25 @@ class DocumentationParser { $urlParts = parse_url($url); if($urlParts && isset($urlParts['scheme'])) continue; - // Rewrite URL - if(preg_match('/^\//', $url)) { - // Absolute: Only path to module base - $relativeUrl = Controller::join_links($baselink, $url); - } else { - // Relative: Include path to module base and any folders - $relativeUrl = Controller::join_links($baselink, $relativePath, $url); + // for images we need to use the file base path + if(preg_match('/_images/', $url)) { + $relativeUrl = Controller::join_links( + Director::absoluteBaseURL(), + $fileBaseLink, + $url + ); } - + else { + // Rewrite public URL + if(preg_match('/^\//', $url)) { + // Absolute: Only path to module base + $relativeUrl = Controller::join_links($baselink, $url); + } else { + // Relative: Include path to module base and any folders + $relativeUrl = Controller::join_links($baselink, $relativePath, $url); + } + } + // Resolve relative paths while(strpos($relativeUrl, '..') !== FALSE) { $relativeUrl = preg_replace('/\w+\/\.\.\//', '', $relativeUrl); diff --git a/code/DocumentationService.php b/code/DocumentationService.php index f4cc5ff..233d5a2 100755 --- a/code/DocumentationService.php +++ b/code/DocumentationService.php @@ -36,7 +36,7 @@ class DocumentationService { * * @var array */ - public static $valid_markdown_extensions = array('.md', '.txt', '.markdown'); + public static $valid_markdown_extensions = array('md', 'txt', 'markdown'); /** * Registered {@link DocumentationEntity} objects to include in the documentation. @@ -78,6 +78,16 @@ class DocumentationService { return self::$valid_markdown_extensions; } + /** + * Check to see if a given extension is a valid extension to be rendered. + * Assumes that $ext has a leading dot as that is what $valid_extension uses. + * + * @return bool + */ + public static function is_valid_extension($ext) { + return in_array(strtolower($ext), self::get_valid_extensions()); + } + /** * Set the ignored files list * @@ -313,35 +323,27 @@ class DocumentationService { private static function find_page_recursive($base, $goal) { $handle = opendir($base); - $name = strtolower(array_shift($goal)); + $name = self::trim_extension_off(strtolower(array_shift($goal))); if(!$name || $name == '/') $name = 'index'; - if($handle) { - $extensions = DocumentationService::get_valid_extensions(); + if($handle) { + $ignored = self::get_ignored_files(); + // ensure we end with a slash $base = rtrim($base, '/') .'/'; while (false !== ($file = readdir($handle))) { - if(in_array($file, DocumentationService::get_valid_extensions())) continue; + if(in_array($file, $ignored)) continue; - $formatted = strtolower($file); - - // if the name has a . then take the substr - $formatted = ($pos = strrpos($formatted, '.')) ? substr($formatted, 0, $pos) : $formatted; - - if($dot = strrpos($name, '.')) { - if(in_array(substr($name, $dot), self::get_valid_extensions())) { - $name = substr($name, 0, $dot); - } - } + $formatted = self::trim_extension_off(strtolower($file)); // the folder is the one that we are looking for. if(strtolower($name) == strtolower($formatted)) { // if this file is a directory we could be displaying that // or simply moving towards the goal. - if(is_dir($base . $file)) { + if(is_dir(Controller::join_links($base, $file))) { $base = $base . trim($file, '/') .'/'; @@ -402,30 +404,36 @@ class DocumentationService { } /** - * Helper function to strip the extension off. - * Warning: Doesn't work if the filename includes dots, - * but no extension, e.g. "2.4.0-alpha" will return "2.4". + * Helper function to strip the extension off and return the name without + * the extension. If you need the extension see {@link get_extension()} * * @param string * * @return string */ public static function trim_extension_off($name) { - $hasExtension = strrpos($name, '.'); + $ext = self::get_extension($name); - if($hasExtension !== false && $hasExtension > 0) { - $shorted = substr($name, $hasExtension); - - // can remove the extension only if we know how - // to read it again - if(in_array(rtrim($shorted, '/'), self::get_valid_extensions())) { - $name = substr($name, 0, $hasExtension); + if($ext) { + if(self::is_valid_extension($ext)) { + return substr($name, 0, strrpos($name,'.')); } } return $name; } + /** + * Returns the extension from a string. If you want to trim the extension + * off the end of the string see {@link trim_extension_off()} + * + * @param string + * + * @return string + */ + public static function get_extension($name) { + return substr(strrchr($name,'.'), 1); + } /** * Return the children from a given entity sorted by Title using natural ordering. @@ -499,7 +507,6 @@ class DocumentationService { $handle = opendir($folder); if($handle) { - $extensions = self::get_valid_extensions(); $ignore = self::get_ignored_files(); $files = array(); @@ -510,11 +517,13 @@ class DocumentationService { $relativeFilePath = Controller::join_links($relative, $file); if(is_dir($path)) { + // dir $pages[] = $relativeFilePath; if($recusive) self::get_pages_from_folder_recursive($base, $relativeFilePath, $recusive, $pages); } - else if(in_array(substr($file, (strrpos($file, '.'))), $extensions)) { + else if(self::is_valid_extension(self::get_extension($path))) { + // file we want $pages[] = $relativeFilePath; } } diff --git a/code/controllers/DocumentationViewer.php b/code/controllers/DocumentationViewer.php index ed1a59a..696127c 100755 --- a/code/controllers/DocumentationViewer.php +++ b/code/controllers/DocumentationViewer.php @@ -141,7 +141,9 @@ class DocumentationViewer extends Controller { if($firstParam) { // allow assets - if($firstParam == "assets") return parent::handleRequest($request); + if($firstParam == "assets") { + return parent::handleRequest($request); + } // check for permalinks if($link = DocumentationPermalinks::map($firstParam)) { @@ -329,9 +331,6 @@ class DocumentationViewer extends Controller { * @return DataObject */ function getEntities($version = false, $lang = false) { - if(!$version) $version = $this->getVersion(); - if(!$lang) $lang = $this->getLang(); - $entities = DocumentationService::get_registered_entities($version, $lang); $output = new DataObjectSet(); @@ -342,7 +341,7 @@ class DocumentationViewer extends Controller { $mode = ($entity === $currentEntity) ? 'current' : 'link'; $folder = $entity->getFolder(); - $link = $this->Link(array_slice($this->Remaining, -1, -1), $folder, $version, $lang); + $link = $this->Link(array(), $folder, false, $lang); $content = false; if($page = $entity->getIndexPage($version, $lang)) { @@ -602,6 +601,8 @@ class DocumentationViewer extends Controller { if($pages) { $path = array(); + $version = $this->getVersion(); + $lang = $this->getLang(); foreach($pages as $i => $title) { if($title) { @@ -610,7 +611,7 @@ class DocumentationViewer extends Controller { $output->push(new ArrayData(array( 'Title' => DocumentationService::clean_page_name($title), - 'Link' => rtrim($this->Link($path), "/"). "/" + 'Link' => rtrim($this->Link($path, false, $version, $lang), "/"). "/" ))); } } @@ -651,7 +652,9 @@ class DocumentationViewer extends Controller { public function Link($path = false, $entity = false, $version = false, $lang = false) { $base = Director::absoluteBaseURL(); - $version = (!$version) ? $this->getVersion() : $version; + // only include the version. Version is optional after all + $version = ($version === null) ? $this->getVersion() : $version; + $lang = (!$lang) ? $this->getLang() : $lang; $entity = (!$entity && $this->entity) ? $this->entity : $entity; diff --git a/code/models/DocumentationPage.php b/code/models/DocumentationPage.php index 0160f4e..2a53602 100755 --- a/code/models/DocumentationPage.php +++ b/code/models/DocumentationPage.php @@ -66,6 +66,13 @@ class DocumentationPage extends ViewableData { $this->relativePath = $path; } + /** + * @return string + */ + function getExtension() { + return DocumentationService::get_extension($this->getRelativePath()); + } + /** * Absolute path including version and lang folder. * @@ -209,17 +216,23 @@ class DocumentationPage extends ViewableData { } /** - * Return the raw markdown for a given documentation page + * Return the raw markdown for a given documentation page. Will throw + * an error if the path isn't a file. * - * @throws InvalidArgumentException - * @return String + * Will return empty if the type is not readable + * + * @return string */ function getMarkdown() { try { $path = $this->getPath(true); if($path) { - return file_get_contents($path); + $ext = $this->getExtension(); + + if(DocumentationService::is_valid_extension($ext)) { + return file_get_contents($path); + } } } catch(InvalidArgumentException $e) {} diff --git a/docs/en/Configuration-Options.md b/docs/en/Configuration-Options.md index 90fff28..89aaf64 100755 --- a/docs/en/Configuration-Options.md +++ b/docs/en/Configuration-Options.md @@ -1,33 +1,56 @@ -# Helpful Configuration Options +# Configuration Options - DocumentationService::set_ignored_files(array()); +## Registering what to document -If you want to ignore (hide) certain file types from being included. +By default the documentation system will parse all the directories in your project +and include the documentation. If you want to only specify a few folders you can +disable it and register your paths manually + :::php + // turns off automatic parsing of filesystem 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) + // registers module 'sapphire' + try { + DocumentationService::register("sapphire", BASE_PATH ."/sapphire/docs/", 'trunk'); + + } catch(InvalidArgumentException $e) { + + } + -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. +If you only want to disable documentation for one module you can correspondingly +call unregister() + + :::php + DocumentationService::unregister($module, $version = false, $lang = false) + +Unregister a module. 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. + + +## Hiding files from listing + +If you want to ignore (hide) certain file types from being included in the +listings. By default this is the list of hidden files + + :::php + $files = array( + '.', '..', '.DS_Store', '.svn', '.git', 'assets', 'themes', '_images' + ); + + DocumentationService::set_ignored_files($files); ## Permalinks -You can set short names for longer urls so they are easier to remember. Set the following in your mysite/_config.php file: +Permalinks can be setup to make nicer urls or to help redirect older urls +to new structures. DocumentationPermalinks::add(array( - 'debugging' => 'current/en/sapphire/topics/debugging', - 'templates' => 'current/en/sapphire/topics/templates' + 'debugging' => 'sapphire/en/topics/debugging', + 'templates' => 'sapphire/en/topics/templates' )); diff --git a/docs/en/Writing-Documentation.md b/docs/en/Writing-Documentation.md index 3d5ec60..21e2acb 100755 --- a/docs/en/Writing-Documentation.md +++ b/docs/en/Writing-Documentation.md @@ -1,32 +1,67 @@ -# Writing Documentation # +# 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/. +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/en/. -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). +The files have to end with the __.md__ extension. The documentation viewer will +automatically replace hyphens (-) with spaces. -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 + my-documentation-file.md + +Translates to: + + My documentation file + +The module also support number prefixing for specifying the order of pages in +the index pages and navigation trees. + + 03-foo.md + 1-bar.md + 4-baz.md + +Will be output as the following in the listing views. + + Bar + Foo + Baz + +## Localization + +All documentation folder should be localized. Even if you do not plan on supporting +multiple languages you need to write your documentation in a 'en' subfolder /module/docs/en/ + -## Syntax ## +## Syntax -This uses a customized markdown extra parser. To view the syntax for page formatting check out [Daring Fireball](http://daringfireball.net/projects/markdown/syntax) +Documentation should be written in markdown with an `.md` extension attached. +To view the syntax for page formatting check out [Daring Fireball](http://daringfireball.net/projects/markdown/syntax). -## Creating Hierarchy ## +To see how to use the documentation from examples, I recommend opening up this +file in your text editor and playing around. As these files are plain text, any +text editor will be able to open and write markdown files. + + +## Creating Hierarchy + +The document viewer supports a hierarchical folder structure so you can categorize +documentation and create topics. + +## Directory Listing + +Each folder you create should also contain a __index.md__ file which contains +an overview of the module and related links. If no index is available, the +default behaviour is to display an ordered list of links. + +## Table of Contents + +The table of contents on each module page is generated based on where and what +headers you use. -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 ## +## Images and Files -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 +If you want to attach images and other assets to a page you need to bundle those +in a directory called _images at the same level as your documentation. \ No newline at end of file diff --git a/templates/Includes/DocInThisModule.ss b/templates/Includes/DocInThisModule.ss index 4508d66..4167d3c 100755 --- a/templates/Includes/DocInThisModule.ss +++ b/templates/Includes/DocInThisModule.ss @@ -1,23 +1,26 @@ - \ No newline at end of file +<% if EntityPages %> + +<% end_if %> \ No newline at end of file diff --git a/tests/DocumentationParserTest.php b/tests/DocumentationParserTest.php index 31dc735..c7d9707 100755 --- a/tests/DocumentationParserTest.php +++ b/tests/DocumentationParserTest.php @@ -237,4 +237,24 @@ HTML; $parsed ); } + + function testRewritingRelativeLinksToFiles() { + $folder = '/sapphiredocs/tests/docs-parser/'; + + $page = new DocumentationPage(); + $page->setRelativePath('file-download.md'); + $page->setEntity(new DocumentationEntity('parser', '2.4', BASE_PATH . $folder)); + + $parsed = DocumentationParser::parse($page, $folder); + + $this->assertContains( + Controller::join_links($folder, '/en/_images/external_link.png'), + $parsed + ); + + $this->assertContains( + Controller::join_links($folder, '/en/_images/test.tar.gz'), + $parsed + ); + } } \ No newline at end of file diff --git a/tests/DocumentationServiceTest.php b/tests/DocumentationServiceTest.php index 9726b6e..c270f61 100755 --- a/tests/DocumentationServiceTest.php +++ b/tests/DocumentationServiceTest.php @@ -11,7 +11,6 @@ class DocumentationServiceTest extends SapphireTest { $entity = DocumentationService::register('testdocs', BASE_PATH . '/sapphiredocs/tests/docs/'); $pages = DocumentationService::get_pages_from_folder($entity); - // check folders and files exist as their filenames $this->assertContains('index.md', $pages->column('Filename'), 'The tests/docs/en folder should contain a index file'); $this->assertContains('subfolder/', $pages->column('Filename'), 'The tests/docs/en folder should contain a subfolder called subfolder'); $this->assertContains('test.md', $pages->column('Filename'), 'The tests/docs/en folder should contain a test file'); @@ -96,4 +95,18 @@ class DocumentationServiceTest extends SapphireTest { $this->assertEquals(DocumentationService::clean_page_name($value), $should[$key]); } } + + function testIsValidExtension() { + $this->assertTrue(DocumentationService::is_valid_extension('md')); + $this->assertTrue(DocumentationService::is_valid_extension('markdown')); + $this->assertTrue(DocumentationService::is_valid_extension('MD')); + $this->assertTrue(DocumentationService::is_valid_extension('MARKDOWN')); + + $this->assertFalse(DocumentationService::is_valid_extension('.markd')); + $this->assertFalse(DocumentationService::is_valid_extension('.exe')); + + // require an extension as internally we check for extension, not using + // one could cause issues. + $this->assertFalse(DocumentationService::is_valid_extension('')); + } } \ No newline at end of file diff --git a/tests/DocumentationViewerTest.php b/tests/DocumentationViewerTest.php index 4c5ee38..75b65d1 100755 --- a/tests/DocumentationViewerTest.php +++ b/tests/DocumentationViewerTest.php @@ -29,6 +29,9 @@ class DocumentationViewerTest extends FunctionalTest { DocumentationService::register("DocumentationViewerTests", BASE_PATH . "/sapphiredocs/tests/docs/", '2.3'); DocumentationService::register("DocumentationViewerTests", BASE_PATH . "/sapphiredocs/tests/docs-v2.4/", '2.4', 'Doc Test', true); DocumentationService::register("DocumentationViewerTests", BASE_PATH . "/sapphiredocs/tests/docs-v3.0/", '3.0', 'Doc Test'); + + DocumentationService::register("DocumentationViewerAltModule1", BASE_PATH . "/sapphiredocs/tests/docs-parser/", '1.0'); + DocumentationService::register("DocumentationViewerAltModule2", BASE_PATH . "/sapphiredocs/tests/docs-search/", '1.0'); } function tearDownOnce() { @@ -113,7 +116,7 @@ class DocumentationViewerTest extends FunctionalTest { $this->assertEquals('Subfolder', $page->Title); } - function testGetModulePages() { + function testGetEntityPages() { $v = new DocumentationViewer(); $response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/2.3/subfolder/')); $pages = $v->getEntityPages(); @@ -231,4 +234,20 @@ class DocumentationViewerTest extends FunctionalTest { $response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/3.0')); $this->assertEquals('3.0', $v->getVersion()); } + + function testGetEntities() { + $v = new DocumentationViewer(); + $response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/2.4')); + + $pages = $v->getEntities(); + + $this->assertEquals(3, $pages->Count(), 'Registered 3 entities'); + + // check to see the links don't have version or pages in them + foreach($pages as $page) { + $expected = Controller::join_links('docs', $page->Title, 'en'); + + $this->assertStringEndsWith($expected, $page->Link); + } + } } \ No newline at end of file diff --git a/tests/docs-parser/en/_images/external_link.png b/tests/docs-parser/en/_images/external_link.png new file mode 100644 index 0000000..16f9b92 Binary files /dev/null and b/tests/docs-parser/en/_images/external_link.png differ diff --git a/tests/docs-parser/en/_images/test.tar.gz b/tests/docs-parser/en/_images/test.tar.gz new file mode 100644 index 0000000..261198e Binary files /dev/null and b/tests/docs-parser/en/_images/test.tar.gz differ diff --git a/tests/docs-parser/en/file-download.md b/tests/docs-parser/en/file-download.md new file mode 100644 index 0000000..82ba68e --- /dev/null +++ b/tests/docs-parser/en/file-download.md @@ -0,0 +1,6 @@ +# File test + +[image](_images/external_link.png) + +[tar.gz](_images/test.tar.gz) + diff --git a/tests/docs/en/_images/external_link.png b/tests/docs/en/_images/external_link.png new file mode 100644 index 0000000..16f9b92 Binary files /dev/null and b/tests/docs/en/_images/external_link.png differ