BUGFIX: fixed bug with linking to internal assets.

This commit is contained in:
Will Rossiter 2011-07-04 16:58:15 +12:00
parent cb19549a97
commit 96ac20a15c
14 changed files with 270 additions and 113 deletions

View File

@ -270,6 +270,9 @@ class DocumentationParser {
$relativePath = dirname($page->getRelativePath()); $relativePath = dirname($page->getRelativePath());
if($relativePath == '.') $relativePath = ''; if($relativePath == '.') $relativePath = '';
// file base link
$fileBaseLink = Director::makeRelative(dirname($page->getPath()));
if($matches) { if($matches) {
foreach($matches[0] as $i => $match) { foreach($matches[0] as $i => $match) {
$title = $matches[2][$i]; $title = $matches[2][$i];
@ -282,15 +285,25 @@ class DocumentationParser {
$urlParts = parse_url($url); $urlParts = parse_url($url);
if($urlParts && isset($urlParts['scheme'])) continue; if($urlParts && isset($urlParts['scheme'])) continue;
// Rewrite URL // for images we need to use the file base path
if(preg_match('/^\//', $url)) { if(preg_match('/_images/', $url)) {
// Absolute: Only path to module base $relativeUrl = Controller::join_links(
$relativeUrl = Controller::join_links($baselink, $url); Director::absoluteBaseURL(),
} else { $fileBaseLink,
// Relative: Include path to module base and any folders $url
$relativeUrl = Controller::join_links($baselink, $relativePath, $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 // Resolve relative paths
while(strpos($relativeUrl, '..') !== FALSE) { while(strpos($relativeUrl, '..') !== FALSE) {
$relativeUrl = preg_replace('/\w+\/\.\.\//', '', $relativeUrl); $relativeUrl = preg_replace('/\w+\/\.\.\//', '', $relativeUrl);

View File

@ -36,7 +36,7 @@ class DocumentationService {
* *
* @var array * @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. * Registered {@link DocumentationEntity} objects to include in the documentation.
@ -78,6 +78,16 @@ class DocumentationService {
return self::$valid_markdown_extensions; return self::$valid_markdown_extensions;
} }
/**
* Check to see if a given extension is a valid extension to be rendered.
* Assumes that $ext has a leading dot as that is what $valid_extension uses.
*
* @return bool
*/
public static function is_valid_extension($ext) {
return in_array(strtolower($ext), self::get_valid_extensions());
}
/** /**
* Set the ignored files list * Set the ignored files list
* *
@ -313,35 +323,27 @@ class DocumentationService {
private static function find_page_recursive($base, $goal) { private static function find_page_recursive($base, $goal) {
$handle = opendir($base); $handle = opendir($base);
$name = strtolower(array_shift($goal)); $name = self::trim_extension_off(strtolower(array_shift($goal)));
if(!$name || $name == '/') $name = 'index'; 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 // ensure we end with a slash
$base = rtrim($base, '/') .'/'; $base = rtrim($base, '/') .'/';
while (false !== ($file = readdir($handle))) { while (false !== ($file = readdir($handle))) {
if(in_array($file, DocumentationService::get_valid_extensions())) continue; if(in_array($file, $ignored)) continue;
$formatted = strtolower($file); $formatted = self::trim_extension_off(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);
}
}
// the folder is the one that we are looking for. // the folder is the one that we are looking for.
if(strtolower($name) == strtolower($formatted)) { if(strtolower($name) == strtolower($formatted)) {
// if this file is a directory we could be displaying that // if this file is a directory we could be displaying that
// or simply moving towards the goal. // or simply moving towards the goal.
if(is_dir($base . $file)) { if(is_dir(Controller::join_links($base, $file))) {
$base = $base . trim($file, '/') .'/'; $base = $base . trim($file, '/') .'/';
@ -402,30 +404,36 @@ class DocumentationService {
} }
/** /**
* Helper function to strip the extension off. * Helper function to strip the extension off and return the name without
* Warning: Doesn't work if the filename includes dots, * the extension. If you need the extension see {@link get_extension()}
* but no extension, e.g. "2.4.0-alpha" will return "2.4".
* *
* @param string * @param string
* *
* @return string * @return string
*/ */
public static function trim_extension_off($name) { public static function trim_extension_off($name) {
$hasExtension = strrpos($name, '.'); $ext = self::get_extension($name);
if($hasExtension !== false && $hasExtension > 0) { if($ext) {
$shorted = substr($name, $hasExtension); if(self::is_valid_extension($ext)) {
return substr($name, 0, strrpos($name,'.'));
// 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);
} }
} }
return $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. * Return the children from a given entity sorted by Title using natural ordering.
@ -499,7 +507,6 @@ class DocumentationService {
$handle = opendir($folder); $handle = opendir($folder);
if($handle) { if($handle) {
$extensions = self::get_valid_extensions();
$ignore = self::get_ignored_files(); $ignore = self::get_ignored_files();
$files = array(); $files = array();
@ -510,11 +517,13 @@ class DocumentationService {
$relativeFilePath = Controller::join_links($relative, $file); $relativeFilePath = Controller::join_links($relative, $file);
if(is_dir($path)) { if(is_dir($path)) {
// dir
$pages[] = $relativeFilePath; $pages[] = $relativeFilePath;
if($recusive) self::get_pages_from_folder_recursive($base, $relativeFilePath, $recusive, $pages); 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; $pages[] = $relativeFilePath;
} }
} }

View File

@ -141,7 +141,9 @@ class DocumentationViewer extends Controller {
if($firstParam) { if($firstParam) {
// allow assets // allow assets
if($firstParam == "assets") return parent::handleRequest($request); if($firstParam == "assets") {
return parent::handleRequest($request);
}
// check for permalinks // check for permalinks
if($link = DocumentationPermalinks::map($firstParam)) { if($link = DocumentationPermalinks::map($firstParam)) {
@ -329,9 +331,6 @@ class DocumentationViewer extends Controller {
* @return DataObject * @return DataObject
*/ */
function getEntities($version = false, $lang = false) { function getEntities($version = false, $lang = false) {
if(!$version) $version = $this->getVersion();
if(!$lang) $lang = $this->getLang();
$entities = DocumentationService::get_registered_entities($version, $lang); $entities = DocumentationService::get_registered_entities($version, $lang);
$output = new DataObjectSet(); $output = new DataObjectSet();
@ -342,7 +341,7 @@ class DocumentationViewer extends Controller {
$mode = ($entity === $currentEntity) ? 'current' : 'link'; $mode = ($entity === $currentEntity) ? 'current' : 'link';
$folder = $entity->getFolder(); $folder = $entity->getFolder();
$link = $this->Link(array_slice($this->Remaining, -1, -1), $folder, $version, $lang); $link = $this->Link(array(), $folder, false, $lang);
$content = false; $content = false;
if($page = $entity->getIndexPage($version, $lang)) { if($page = $entity->getIndexPage($version, $lang)) {
@ -602,6 +601,8 @@ class DocumentationViewer extends Controller {
if($pages) { if($pages) {
$path = array(); $path = array();
$version = $this->getVersion();
$lang = $this->getLang();
foreach($pages as $i => $title) { foreach($pages as $i => $title) {
if($title) { if($title) {
@ -610,7 +611,7 @@ class DocumentationViewer extends Controller {
$output->push(new ArrayData(array( $output->push(new ArrayData(array(
'Title' => DocumentationService::clean_page_name($title), '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) { public function Link($path = false, $entity = false, $version = false, $lang = false) {
$base = Director::absoluteBaseURL(); $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; $lang = (!$lang) ? $this->getLang() : $lang;
$entity = (!$entity && $this->entity) ? $this->entity : $entity; $entity = (!$entity && $this->entity) ? $this->entity : $entity;

View File

@ -66,6 +66,13 @@ class DocumentationPage extends ViewableData {
$this->relativePath = $path; $this->relativePath = $path;
} }
/**
* @return string
*/
function getExtension() {
return DocumentationService::get_extension($this->getRelativePath());
}
/** /**
* Absolute path including version and lang folder. * 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 * Will return empty if the type is not readable
* @return String *
* @return string
*/ */
function getMarkdown() { function getMarkdown() {
try { try {
$path = $this->getPath(true); $path = $this->getPath(true);
if($path) { if($path) {
return file_get_contents($path); $ext = $this->getExtension();
if(DocumentationService::is_valid_extension($ext)) {
return file_get_contents($path);
}
} }
} }
catch(InvalidArgumentException $e) {} catch(InvalidArgumentException $e) {}

View File

@ -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); DocumentationService::set_automatic_registration(false);
By default the documentation system will parse all the directories in your project and // registers module 'sapphire'
include the documentation. If you want to only specify a few folders you can disable it try {
with the above. DocumentationService::register("sapphire", BASE_PATH ."/sapphire/docs/", 'trunk');
DocumentationService::register($module, $path, $version = 'current', $lang = 'en', $major_release = false) } catch(InvalidArgumentException $e) {
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 If you only want to disable documentation for one module you can correspondingly
and the lang. If no version is specified then all folders of that lang are removed. If you do call unregister()
not specify a version or lang the whole module will be removed from the documentation.
:::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 ## 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( DocumentationPermalinks::add(array(
'debugging' => 'current/en/sapphire/topics/debugging', 'debugging' => 'sapphire/en/topics/debugging',
'templates' => 'current/en/sapphire/topics/templates' 'templates' => 'sapphire/en/topics/templates'
)); ));

View File

@ -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 Your documentation needs to go in the specific modules docs folder which it
a feature of your custom module 'MyModule' you need to create markdown files in mymodule/docs/. 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 The files have to end with the __.md__ extension. The documentation viewer will
have spaces web / file systems). 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 my-documentation-file.md
in a 'en' subfolder
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/ /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 If you want to attach images and other assets to a page you need to bundle those
module and related links. in a directory called _images at the same level as your documentation.
## Table of Contents ##
The table of contents on each module page is generated

View File

@ -1,23 +1,26 @@
<div id="sibling-pages" class="sidebar-box"> <% if EntityPages %>
<ul> <div id="sibling-pages" class="sidebar-box">
<% control EntityPages %> <h4>In this module:</h4>
<li> <ul>
<a href="$Link" class="$LinkingMode">$Title</a> <% control EntityPages %>
<% if Children %> <li>
<ul> <a href="$Link" class="$LinkingMode">$Title</a>
<% control Children %> <% if Children %>
<li><a href="$Link" class="$LinkingMode">$Title</a> <ul>
<% if Children %> <% control Children %>
<ul> <li><a href="$Link" class="$LinkingMode">$Title</a>
<% control Children %> <% if Children %>
<li><a href="$Link" class="$LinkingMode">$Title</a></li> <ul>
<% end_control %> <% control Children %>
</ul><% end_if %> <li><a href="$Link" class="$LinkingMode">$Title</a></li>
</li> <% end_control %>
<% end_control %> </ul><% end_if %>
</ul> </li>
<% end_if %> <% end_control %>
</li> </ul>
<% end_control %> <% end_if %>
</ul> </li>
</div> <% end_control %>
</ul>
</div>
<% end_if %>

View File

@ -237,4 +237,24 @@ HTML;
$parsed $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
);
}
} }

View File

@ -11,7 +11,6 @@ class DocumentationServiceTest extends SapphireTest {
$entity = DocumentationService::register('testdocs', BASE_PATH . '/sapphiredocs/tests/docs/'); $entity = DocumentationService::register('testdocs', BASE_PATH . '/sapphiredocs/tests/docs/');
$pages = DocumentationService::get_pages_from_folder($entity); $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('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('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'); $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]); $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(''));
}
} }

View File

@ -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/", '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-v2.4/", '2.4', 'Doc Test', true);
DocumentationService::register("DocumentationViewerTests", BASE_PATH . "/sapphiredocs/tests/docs-v3.0/", '3.0', 'Doc Test'); 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() { function tearDownOnce() {
@ -113,7 +116,7 @@ class DocumentationViewerTest extends FunctionalTest {
$this->assertEquals('Subfolder', $page->Title); $this->assertEquals('Subfolder', $page->Title);
} }
function testGetModulePages() { function testGetEntityPages() {
$v = new DocumentationViewer(); $v = new DocumentationViewer();
$response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/2.3/subfolder/')); $response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/2.3/subfolder/'));
$pages = $v->getEntityPages(); $pages = $v->getEntityPages();
@ -231,4 +234,20 @@ class DocumentationViewerTest extends FunctionalTest {
$response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/3.0')); $response = $v->handleRequest(new SS_HTTPRequest('GET', 'DocumentationViewerTests/en/3.0'));
$this->assertEquals('3.0', $v->getVersion()); $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);
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

View File

@ -0,0 +1,6 @@
# File test
[image](_images/external_link.png)
[tar.gz](_images/test.tar.gz)

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B