Add the ability to do relative links in documentations

This commit is contained in:
Stéphane Goetz 2016-07-04 20:33:44 +02:00
parent 563fcbca94
commit c1dd70748f
15 changed files with 433 additions and 47 deletions

View File

@ -28,6 +28,7 @@
} }
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "~4" "phpunit/phpunit": "~4",
"mikey179/vfsStream": "^1.6"
} }
} }

50
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "bd4fe65872a143039090e3c62b4c5a40", "hash": "31e312c5eb200fe262cc2557d2b7a5c6",
"content-hash": "de3684c6c65b1602b52e6534aa5e6eda", "content-hash": "cae1a4262309f9c2953036c6a756f04b",
"packages": [ "packages": [
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
@ -667,6 +667,52 @@
], ],
"time": "2015-06-14 21:17:01" "time": "2015-06-14 21:17:01"
}, },
{
"name": "mikey179/vfsStream",
"version": "v1.6.3",
"source": {
"type": "git",
"url": "https://github.com/mikey179/vfsStream.git",
"reference": "c19925cd0390d3c436a0203ae859afa460d0474b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mikey179/vfsStream/zipball/c19925cd0390d3c436a0203ae859afa460d0474b",
"reference": "c19925cd0390d3c436a0203ae859afa460d0474b",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "~4.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
}
},
"autoload": {
"psr-0": {
"org\\bovigo\\vfs\\": "src/main/php"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Frank Kleine",
"homepage": "http://frankkleine.de/",
"role": "Developer"
}
],
"description": "Virtual file system to mock the real file system in unit tests.",
"homepage": "http://vfs.bovigo.org/",
"time": "2016-04-09 09:42:01"
},
{ {
"name": "phpdocumentor/reflection-docblock", "name": "phpdocumentor/reflection-docblock",
"version": "2.0.4", "version": "2.0.4",

View File

@ -6,20 +6,20 @@
### For Authors ### For Authors
* [Auto Generated Navigation / Page sorting](!Features/Navigation_and_Sorting) * [Auto Generated Navigation / Page sorting](01_Features/Navigation_and_Sorting.md)
* [Internal documentation links](!Features/Internal_links) * [Internal documentation links](01_Features/Internal_links.md)
* [Github Flavored Markdown](!Features/GitHub_Flavored_Markdown) * [Github Flavored Markdown](01_Features/GitHub_Flavored_Markdown.md)
* [Auto created homepage/landing page](!Features/Landing_page) * [Auto created homepage/landing page](01_Features/Landing_page.md)
* [Multiple Output Formats](!Features/Multiple_Output_Formats) * [Multiple Output Formats](01_Features/Multiple_Output_Formats.md)
* [Multiple Languages Support](!Features/Multilanguage) * [Multiple Languages Support](01_Features/Multilanguage.md)
* [No Build Step](!Features/Live_mode) * [No Build Step](01_Features/Live_mode.md)
* [Static Output Generation](!Features/Static_Site_Generation) * [Static Output Generation](01_Features/Static_Site_Generation.md)
* [Table of Contents](!Features/Table_of_contents) * [Table of Contents](01_Features/Table_of_contents.md)
### For Developers ### For Developers
* [Auto Syntax Highlighting](!Features/Auto_Syntax_Highlight) * [Auto Syntax Highlighting](01_Features/Auto_Syntax_Highlight.md)
* [Extend Daux.io with Processors](!For_Developers/Creating_a_Processor) * [Extend Daux.io with Processors](01_For_Developers/Creating_a_Processor.md)
* Full access to the internal API to create new pages programatically * Full access to the internal API to create new pages programatically
* Work with pages metadata * Work with pages metadata
@ -97,7 +97,7 @@ You can use PHP's embedded web server by running the following command in the ro
Upload your files to an apache / nginx server and see your documentation Upload your files to an apache / nginx server and see your documentation
[More informations here](!Features/Live_mode) [More informations here](01_Features/Live_mode.md)
#### Export to other formats #### Export to other formats
@ -107,7 +107,7 @@ Daux.io is extendable and comes by default with three export formats:
- Export all documentation in a single HTML page - Export all documentation in a single HTML page
- Upload to your Atlassian Confluence server. - Upload to your Atlassian Confluence server.
[See a detailed feature comparison matrix](!Features/Multiple_Output_Formats) [See a detailed feature comparison matrix](01_Features/Multiple_Output_Formats.md)
Here's how you run an export: Here's how you run an export:
@ -115,11 +115,11 @@ Here's how you run an export:
./generate ./generate
``` ```
[See here for all options](!Features/Static_Site_Generation) [See here for all options](01_Features/Static_Site_Generation.md)
## Configuration ## Configuration
Now that you got the basics, you can also [see what you can configure](!Configuration) Now that you got the basics, you can also [see what you can configure](05_Configuration/_index.md)
## Known Issues ## Known Issues

View File

@ -18,4 +18,4 @@ Here is a quick example :
<!-- here goes the rest of the page --> <!-- here goes the rest of the page -->
</body> </body>
[See the full list of supported languages in Daux.io](!Examples/Code_Highlighting) [See the full list of supported languages in Daux.io](../02_Examples/Code_Highlighting.md)

View File

@ -1,7 +1,49 @@
You can create links from a page to an other, the link is then resolved to the real page. You can create links from a page to an other, the link is then resolved to the real page.
Creating a link to another page is done exactly like a normal markdown link. In the url part, start with `!` and set the absolute path to the file, omitting the numbering and file extension Each relative link in your pages will be resolved to a page or content within the documentation.
If the link's destination isn't found, the page generation will fail.
A link to `01_Examples/05_Code_Highlighting.md` Would be written like this: `[Code Highlight Examples](!Examples/Code_Highlighting)` Any valid markdown link is a valid Daux.io link.
The page generation will fail if a link is wrong. If your file structure looks like this:
```
├── 00_Getting_Started.md
├── 01_Features
│ ├── 01_GitHub_Flavored_Markdown.md
├── 02_Examples
│ ├── Hello_World.md
│ ├── 05_Code_Highlighting.md
```
From the page `01_Features/01_GitHub_Flavored_Markdown.md`, all the following links would be valid:
[Getting Started](../00_Getting_Started.md)
[Getting Started](../00_Getting_Started.html)
[Getting Started](../00_Getting_Started)
[Getting Started](../Getting_Started)
// A link starting with / means root of the documentation, not the server it will be served from.
[Getting Started](/Getting_Started.html)
[Getting Started](/Getting_Started)
// These Will first be searched for in the current directory and then start at the root of the documentation
[Getting Started](Getting_Started)
[Getting Started](00_Getting_Started)
[Hello World](../02_Examples/Hello_World.md)
[Hello World](../02_Examples/Hello_World.html)
[Hello World](../02_Examples/Hello_World)
[Hello World](../Examples/Hello_World)
[Hello World](/02_Examples/Hello_World.md)
[Hello World](Examples/Hello_World)
[Hello World](02_Examples/Hello_World)
## Github publishing
If you plan to publish your documentation on Github along with your source code, we recommend to only use relative links with full names.
From the list of links above only these two will work both on Github and on Daux.io
[Getting Started](../00_Getting_Started.md)
[Hello World](../02_Examples/Hello_World.md)

View File

@ -10,23 +10,23 @@
#### For Authors #### For Authors
* [Auto Generated Navigation / Page sorting](!Features/Navigation_and_Sorting) * [Auto Generated Navigation / Page sorting](01_Features/Navigation_and_Sorting.md)
* [Internal documentation links](!Features/Internal_links) * [Internal documentation links](01_Features/Internal_links.md)
* [Github Flavored Markdown](!Features/GitHub_Flavored_Markdown) * [Github Flavored Markdown](01_Features/GitHub_Flavored_Markdown.md)
* [Auto created homepage/landing page](!Features/Landing_page) * [Auto created homepage/landing page](01_Features/Landing_page.md)
* [Multiple Output Formats](!Features/Multiple_Output_Formats) * [Multiple Output Formats](01_Features/Multiple_Output_Formats.md)
* [Multiple Languages Support](!Features/Multilanguage) * [Multiple Languages Support](01_Features/Multilanguage.md)
* [No Build Step](!Features/Live_mode) * [No Build Step](01_Features/Live_mode.md)
* [Static Output Generation](!Features/Static_Site_Generation) * [Static Output Generation](01_Features/Static_Site_Generation.md)
* [Table of Contents](!Features/Table_of_contents) * [Table of Contents](01_Features/Table_of_contents.md)
</div> </div>
<div class=col-third> <div class=col-third>
#### For Developers #### For Developers
* [Auto Syntax Highlighting](!Features/Auto_Syntax_Highlight) * [Auto Syntax Highlighting](01_Features/Auto_Syntax_Highlight.md)
* [Extend Daux.io with Processors](!For_Developers/Creating_a_Processor) * [Extend Daux.io with Processors](01_For_Developers/Creating_a_Processor.md)
* Full access to the internal API to create new pages programatically * Full access to the internal API to create new pages programatically
* Work with pages metadata * Work with pages metadata

View File

@ -1,6 +1,7 @@
<?php namespace Todaymade\Daux; <?php namespace Todaymade\Daux;
use ArrayObject; use ArrayObject;
use Todaymade\Daux\Tree\Content;
class Config extends ArrayObject class Config extends ArrayObject
{ {
@ -46,4 +47,14 @@ class Config extends ArrayObject
{ {
$this->merge($newValues, false); $this->merge($newValues, false);
} }
public function getCurrentPage()
{
return $this['current_page'];
}
public function setCurrentPage(Content $entry)
{
$this['current_page'] = $entry;
}
} }

View File

@ -27,6 +27,7 @@ class ContentType implements \Todaymade\Daux\ContentTypes\ContentType
public function convert($raw, Content $node) public function convert($raw, Content $node)
{ {
$this->config->setCurrentPage($node);
return $this->converter->convertToHtml($raw); return $this->converter->convertToHtml($raw);
} }
} }

View File

@ -28,13 +28,30 @@ class LinkRenderer extends \League\CommonMark\Inline\Renderer\LinkRenderer
*/ */
protected function resolveInternalFile($url) protected function resolveInternalFile($url)
{ {
$file = DauxHelper::getFile($this->daux['tree'], $url); $triedAbsolute = false;
if ($file) {
// Legacy absolute paths could start with
// "!" In this case we will try to find
// the file starting at the root
if ($url[0] == '!' || $url[0] == '/') {
$url = ltrim($url, "!/");
if ($file = DauxHelper::getFile($this->daux['tree'], $url)) {
return $file;
}
$triedAbsolute = true;
}
// Seems it's not an absolute path or not found,
// so we'll continue with the current folder
if ($file = DauxHelper::getFile($this->daux->getCurrentPage()->getParent(), $url)) {
return $file; return $file;
} }
$file = DauxHelper::getFile($this->daux['tree'], $url . '.html'); // If we didn't already try it, we'll
if ($file) { // do a pass starting at the root
if (!$triedAbsolute && $file = DauxHelper::getFile($this->daux['tree'], $url)) {
return $file; return $file;
} }
@ -42,10 +59,10 @@ class LinkRenderer extends \League\CommonMark\Inline\Renderer\LinkRenderer
} }
/** /**
* @param Link $inline * @param AbstractInline|Link $inline
* @param ElementRendererInterface $htmlRenderer * @param ElementRendererInterface $htmlRenderer
*
* @return HtmlElement * @return HtmlElement
* @throws Exception
*/ */
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer) public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{ {
@ -63,12 +80,19 @@ class LinkRenderer extends \League\CommonMark\Inline\Renderer\LinkRenderer
$element = parent::render($inline, $htmlRenderer); $element = parent::render($inline, $htmlRenderer);
$url = $inline->getUrl(); $url = $inline->getUrl();
if (!empty($url) && $url[0] == '!') {
$file = $this->resolveInternalFile(ltrim($url, "!"));
$element->setAttribute('href', $this->daux['base_url'] . $file->getUrl()); // Absolute urls, empty urls and anchors
// should not go through the url resolver
if (empty($url) || $url[0] == "#" || preg_match("|^(?:[a-z]+:)?//|", $url)) {
return $element;
} }
$file = $this->resolveInternalFile($url);
$url = DauxHelper::getRelativePath($this->daux->getCurrentPage()->getUrl(), $file->getUrl());
$element->setAttribute('href', $url);
return $element; return $element;
} }
} }

View File

@ -1,5 +1,6 @@
<?php namespace Todaymade\Daux; <?php namespace Todaymade\Daux;
use Todaymade\Daux\Tree\Builder;
use Todaymade\Daux\Tree\Directory; use Todaymade\Daux\Tree\Directory;
class DauxHelper class DauxHelper
@ -140,6 +141,16 @@ class DauxHelper
return implode(DIRECTORY_SEPARATOR, $absolutes); return implode(DIRECTORY_SEPARATOR, $absolutes);
} }
public static function getFilenames(Config $config, $part)
{
$extensions = implode("|", array_map("preg_quote", $config["valid_content_extensions"])) . "|html";
$raw = preg_replace("/(.*)?\\.(" . $extensions . ")$/", "$1", $part);
$raw = Builder::removeSortingInformations($raw);
return ["$raw.html", $raw];
}
/** /**
* Locate a file in the tree. Returns the file if found or false * Locate a file in the tree. Returns the file if found or false
* *
@ -157,6 +168,16 @@ class DauxHelper
return false; return false;
} }
// Some relative paths may start with ./
if ($node == '.') {
continue;
}
if ($node == '..') {
$tree = $tree->getParent();
continue;
}
$node = urldecode($node); $node = urldecode($node);
// if the node exists in the current request tree, // if the node exists in the current request tree,
@ -167,6 +188,16 @@ class DauxHelper
continue; continue;
} }
// if the node doesn't exist, we can try
// two variants of the requested file:
// with and w/o the .html extension
foreach (static::getFilenames($tree->getConfig(), $node) as $filename) {
if (isset($tree->getEntries()[$filename])) {
$tree = $tree->getEntries()[$filename];
continue 2;
}
}
// At this stage, we're in a directory, but no // At this stage, we're in a directory, but no
// sub-item matches, so the current node must // sub-item matches, so the current node must
// be an index page or we failed // be an index page or we failed
@ -194,7 +225,7 @@ class DauxHelper
* *
* Taken from Stringy * Taken from Stringy
* *
* @param string $title * @param string $title
* @return string * @return string
*/ */
public static function slug($title) public static function slug($title)
@ -357,4 +388,37 @@ class DauxHelper
"\xE2\x80\xAF", "\xE2\x81\x9F", "\xE3\x80\x80"), "\xE2\x80\xAF", "\xE2\x81\x9F", "\xE3\x80\x80"),
); );
} }
public static function getRelativePath($from, $to)
{
// some compatibility fixes for Windows paths
$from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
$to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
$from = str_replace('\\', '/', $from);
$to = str_replace('\\', '/', $to);
$from = explode('/', $from);
$to = explode('/', $to);
$relPath = $to;
foreach ($from as $depth => $dir) {
// find first non-matching dir
if ($dir === $to[$depth]) {
// ignore this directory
array_shift($relPath);
} else {
// get number of remaining dirs to $from
$remaining = count($from) - $depth;
if ($remaining > 1) {
// add traversals up to first matching dir
$padLength = (count($relPath) + $remaining - 1) * -1;
$relPath = array_pad($relPath, $padLength, '..');
break;
} else {
//$relPath[0] = './' . $relPath[0];
}
}
}
return implode('/', $relPath);
}
} }

View File

@ -17,15 +17,17 @@ class Builder
protected static function isIgnored(\SplFileInfo $file, $ignore) protected static function isIgnored(\SplFileInfo $file, $ignore)
{ {
if (in_array($file->getFilename(), static::$IGNORED)) { $filename = $file->getFilename();
if (in_array($filename, static::$IGNORED)) {
return true; return true;
} }
if ($file->isDir() && in_array($file->getFilename(), $ignore['folders'])) { if (array_key_exists('folders', $ignore) && $file->isDir() && in_array($filename, $ignore['folders'])) {
return true; return true;
} }
if (!$file->isDir() && in_array($file->getFilename(), $ignore['files'])) { if (array_key_exists('files', $ignore) && !$file->isDir() && in_array($filename, $ignore['files'])) {
return true; return true;
} }

View File

@ -1,8 +1,10 @@
<?php namespace Todaymade\Daux\Tree; <?php namespace Todaymade\Daux\Tree;
use ArrayIterator;
use RuntimeException;
use Todaymade\Daux\Daux; use Todaymade\Daux\Daux;
class Directory extends Entry class Directory extends Entry implements \ArrayAccess, \IteratorAggregate
{ {
/** @var Entry[] */ /** @var Entry[] */
protected $children = []; protected $children = [];
@ -231,4 +233,54 @@ class Directory extends Entry
return $dump; return $dump;
} }
/**
* Whether a offset exists
* @param mixed $offset An offset to check for.
* @return boolean true on success or false on failure.
*/
public function offsetExists($offset)
{
return array_key_exists($offset, $this->children);
}
/**
* Offset to retrieve
* @param mixed $offset The offset to retrieve.
* @return Entry Can return all value types.
*/
public function offsetGet($offset)
{
return $this->children[$offset];
}
/**
* Offset to set
* @param mixed $offset The offset to assign the value to.
* @param Entry $value The value to set.
* @return void
*/
public function offsetSet($offset, $value)
{
if (!$value instanceof Entry) {
throw new RuntimeException("The value is not of type Entry");
}
$this->addChild($value);
}
/**
* Offset to unset
* @param string $offset the offset to unset
* @return void
*/
public function offsetUnset($offset)
{
unset($this->children[$offset]);
}
public function getIterator()
{
return new ArrayIterator($this->children);
}
} }

View File

@ -0,0 +1,68 @@
<?php namespace Todaymade\Daux\ContentTypes\Markdown;
use org\bovigo\vfs\vfsStream;
use Todaymade\Daux\Config;
use Todaymade\Daux\Daux;
use Todaymade\Daux\DauxHelper;
use Todaymade\Daux\Tree\Builder;
use Todaymade\Daux\Tree\Root;
class LinkRendererTest extends \PHPUnit_Framework_TestCase
{
protected function getTree(Config $config)
{
$structure = [
'Content' => [
'Page.md' => 'some text content',
],
'Widgets' => [
'Page.md' => 'another page',
'Button.md' => 'another page',
],
];
$root = vfsStream::setup('root', null, $structure);
$config['valid_content_extensions'] = ['md'];
$config['mode'] = Daux::STATIC_MODE;
$config['index_key'] = 'index.html';
$tree = new Root($config, $root->url());
Builder::build($tree, []);
return $tree;
}
public function providerRenderLink()
{
return [
// /Widgets/Page
['<a href="http://google.ch">Link</a>', "[Link](http://google.ch)", "Widgets/Page.html"],
['<a href="#features">Link</a>', "[Link](#features)", "Widgets/Page.html"],
['<a href="Button.html">Link</a>', "[Link](Button.md)", "Widgets/Page.html"],
['<a href="Button.html">Link</a>', "[Link](./Button.md)", "Widgets/Page.html"],
['<a href="Button.html">Link</a>', "[Link](Button)", "Widgets/Page.html"],
['<a href="Button.html">Link</a>', "[Link](./Button)", "Widgets/Page.html"],
['<a href="Button.html">Link</a>', "[Link](!Widgets/Button)", "Widgets/Page.html"],
// /Content/Page
['<a href="../Widgets/Button.html">Link</a>', "[Link](../Widgets/Button.md)", "Content/Page.html"],
['<a href="../Widgets/Button.html">Link</a>', "[Link](!Widgets/Button)", "Content/Page.html"],
];
}
/**
* @dataProvider providerRenderLink
*/
public function testRenderLink($expected, $string, $current)
{
$config = new Config();
$config['base_url'] = '';
$config['tree'] = $this->getTree($config);
$config->setCurrentPage(DauxHelper::getFile($config['tree'], $current));
$converter = new CommonMarkConverter(['daux' => $config]);
$this->assertEquals("<p>$expected</p>", trim($converter->convertToHtml($string)));
}
}

25
tests/DauxHelperTest.php Normal file
View File

@ -0,0 +1,25 @@
<?php namespace Todaymade\Daux;
class DauxHelperTest extends \PHPUnit_Framework_TestCase {
public function providerGetFilenames() {
return [
[["Page.html", "Page"], "Page.html"],
[["Page.html", "Page"], "Page.md"],
[["Page.html", "Page"], "Page"],
[["Code_Highlighting.html", "Code_Highlighting"], "05_Code_Highlighting.md"],
];
}
/**
* @dataProvider providerGetFilenames
*/
public function testGetFilenames($expected, $node) {
$config = new Config();
$config['valid_content_extensions'] = ['md'];
$this->assertEquals($expected, DauxHelper::getFilenames($config, $node));
}
}

View File

@ -0,0 +1,50 @@
<?php namespace Todaymade\Daux\Tree;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use Todaymade\Daux\Config;
use Todaymade\Daux\Daux;
class BuilderIntegrationTest extends \PHPUnit_Framework_TestCase
{
/**
* @var vfsStreamDirectory
*/
private $root;
public function setUp()
{
$structure = [
'Contents' => [
'Page.md' => 'some text content',
],
'Widgets' => [
'Page.md' => 'another page',
'Button.md' => 'another page',
],
];
$this->root = vfsStream::setup('root', null, $structure);
}
public function testCreateHierarchy()
{
$config = new Config();
$config['valid_content_extensions'] = ['md'];
$config['mode'] = Daux::STATIC_MODE;
$config['index_key'] = 'index.html';
$tree = new Root($config, $this->root->url());
Builder::build($tree, []);
$this->assertCount(2, $tree);
$this->assertTrue(array_key_exists('Contents', $tree->getEntries()));
$this->assertInstanceOf(Directory::class, $tree['Contents']);
$this->assertTrue(array_key_exists('Widgets', $tree->getEntries()));
$this->assertInstanceOf(Directory::class, $tree['Widgets']);
// TODO :: should not be Page.html, this should not depend on the mode
$this->assertEquals('Page', $tree['Contents']['Page.html']->getTitle());
$this->assertInstanceOf(Content::class, $tree['Contents']['Page.html']);
}
}