Add Table Of Contents feature

This commit is contained in:
Stéphane Goetz 2016-04-12 08:38:52 +02:00
parent 9a615ea55a
commit b037a43e2d
17 changed files with 496 additions and 7 deletions

View File

@ -18,7 +18,8 @@
"league/commonmark": "^0.13",
"symfony/console": "~3.0",
"symfony/finder": "~3.0",
"webuni/commonmark-table-extension": "0.4.*"
"webuni/commonmark-table-extension": "0.4.*",
"myclabs/deep-copy": "^1.5"
},
"autoload": {
"psr-4": {

46
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",
"This file is @generated automatically"
],
"hash": "2587ec4642e574482832632b73b74202",
"content-hash": "598abc0b07c38c31f8cc44bbf293dae3",
"hash": "5d99c57e9efe49df55a765026a15f586",
"content-hash": "88197b6eaf8fc4b266eb8c72b115580a",
"packages": [
{
"name": "guzzlehttp/guzzle",
@ -286,6 +286,48 @@
],
"time": "2015-07-09 02:14:40"
},
{
"name": "myclabs/deep-copy",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "e3abefcd7f106677fd352cd7c187d6c969aa9ddc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e3abefcd7f106677fd352cd7c187d6c969aa9ddc",
"reference": "e3abefcd7f106677fd352cd7c187d6c969aa9ddc",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"doctrine/collections": "1.*",
"phpunit/phpunit": "~4.1"
},
"type": "library",
"autoload": {
"psr-4": {
"DeepCopy\\": "src/DeepCopy/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Create deep copies (clones) of your objects",
"homepage": "https://github.com/myclabs/DeepCopy",
"keywords": [
"clone",
"copy",
"duplicate",
"object",
"object graph"
],
"time": "2015-11-07 22:20:37"
},
{
"name": "react/promise",
"version": "v2.4.0",

View File

@ -1,5 +1,7 @@
**Daux.io** is an documentation generator that uses a simple folder structure and Markdown files to create custom documentation on the fly. It helps you create great looking documentation in a developer friendly way.
[TOC]
## Features
### For Authors
@ -12,6 +14,7 @@
* [Multiple Languages Support](!Features/Multilanguage)
* [No Build Step](!Features/Live_mode)
* [Static Output Generation](!Features/Static_Site_Generation)
* [Table of Contents](!Features/Table_of_contents)
### For Developers

View File

@ -0,0 +1,19 @@
Adding a table of contents becomes very easy with Daux.io
## Automatic
A table of contents can be added automatically to all pages.
If `[TOC]` isn't present it will add it at the beginning of the page.
You can enable this feature in your configuration
```json
{
"auto_toc": true
}
```
## Manual
Add `[TOC]` anywhere in your document and it will be replaced by a table of contents

View File

@ -18,6 +18,7 @@
* [Multiple Languages Support](!Features/Multilanguage)
* [No Build Step](!Features/Live_mode)
* [Static Output Generation](!Features/Static_Site_Generation)
* [Table of Contents](!Features/Table_of_contents)
</div>
<div class="col-sm-4">

View File

@ -18,6 +18,8 @@
"timezone": "America/Los_Angeles",
"auto_toc": false,
"live": {
"inherit_index": false,
"clean_urls": false

View File

@ -3,6 +3,8 @@
use League\CommonMark\DocParser;
use League\CommonMark\Environment;
use League\CommonMark\HtmlRenderer;
use Todaymade\Daux\ContentTypes\Markdown\TOC\Parser;
use Todaymade\Daux\ContentTypes\Markdown\TOC\TOCProcessor;
use Webuni\CommonMark\TableExtension\TableExtension;
class CommonMarkConverter extends \League\CommonMark\CommonMarkConverter
@ -18,6 +20,10 @@ class CommonMarkConverter extends \League\CommonMark\CommonMarkConverter
$environment->mergeConfig($config);
$environment->addExtension(new TableExtension());
// Table of Contents
$environment->addBlockParser(new Parser());
$environment->addDocumentProcessor(new TOCProcessor($config['daux']));
$this->extendEnvironment($environment);
if (array_key_exists('processor_instance', $config['daux'])) {

View File

@ -0,0 +1,50 @@
<?php namespace Todaymade\Daux\ContentTypes\Markdown\TOC;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
class Element extends AbstractBlock
{
/**
* Returns true if this block can contain the given block as a child node
*
* @param AbstractBlock $block
*
* @return bool
*/
public function canContain(AbstractBlock $block)
{
return false;
}
/**
* Returns true if block type can accept lines of text
*
* @return bool
*/
public function acceptsLines()
{
return false;
}
/**
* Whether this is a code block
*
* @return bool
*/
public function isCode()
{
return false;
}
/**
* @param Cursor $cursor
*
* @return bool
*/
public function matchesNextLine(Cursor $cursor)
{
return false;
}
}

View File

@ -0,0 +1,83 @@
<?php namespace Todaymade\Daux\ContentTypes\Markdown\TOC;
use League\CommonMark\Block\Element\Heading;
class Entry
{
protected $content;
protected $level;
protected $parent = null;
protected $children = [];
public function __construct(Heading $content)
{
$this->content = $content;
$this->level = $content->getLevel();
}
/**
* @return string
*/
public function getId()
{
return $this->content->data['attributes']['id'];
}
/**
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* @return Entry
*/
public function getParent()
{
return $this->parent;
}
/**
* @return Heading
*/
public function getContent()
{
return $this->content;
}
/**
* @return Entry[]
*/
public function getChildren()
{
return $this->children;
}
/**
* @param Entry $parent
* @param bool $addChild
*/
public function setParent(Entry $parent, $addChild = true)
{
$this->parent = $parent;
if ($addChild) {
$parent->addChild($this);
}
}
/**
* @param Entry $child
*/
public function addChild(Entry $child)
{
$child->setParent($this, false);
$this->children[] = $child;
}
public function toString()
{
return $this->getLevel() . " - " . $this->getId();
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Created by IntelliJ IDEA.
* User: onigoetz
* Date: 09/04/16
* Time: 23:03
*/
namespace Todaymade\Daux\ContentTypes\Markdown\TOC;
use League\CommonMark\Block\Parser\AbstractBlockParser;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
class Parser extends AbstractBlockParser
{
/**
* @param ContextInterface $context
* @param Cursor $cursor
*
* @return bool
*/
public function parse(ContextInterface $context, Cursor $cursor)
{
if ($cursor->isIndented()) {
return false;
}
$previousState = $cursor->saveState();
$cursor->advanceToFirstNonSpace();
$fence = $cursor->match('/^\[TOC\]/');
if (is_null($fence)) {
$cursor->restoreState($previousState);
return false;
}
$context->addBlock(new Element());
return true;
}
}

View File

@ -0,0 +1,18 @@
<?php namespace Todaymade\Daux\ContentTypes\Markdown\TOC;
class RootEntry extends Entry
{
public function __construct()
{
$this->content = null;
$this->level = 0;
}
/**
* @return Entry
*/
public function getParent()
{
throw new \RuntimeException("No Parent Exception");
}
}

View File

@ -0,0 +1,203 @@
<?php namespace Todaymade\Daux\ContentTypes\Markdown\TOC;
use DeepCopy\DeepCopy;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\Heading;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListData;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\DocumentProcessorInterface;
use League\CommonMark\Inline\Element\Link;
use League\CommonMark\Inline\Element\Text;
use League\CommonMark\Node\Node;
use ReflectionMethod;
use Todaymade\Daux\Config;
use Todaymade\Daux\DauxHelper;
class TOCProcessor implements DocumentProcessorInterface
{
protected $config;
public function __construct(Config $config)
{
$this->config = $config;
}
public function hasAutoTOC()
{
return array_key_exists('auto_toc', $this->config) && $this->config['auto_toc'];
}
/**
* @param Document $document
*
* @return void
*/
public function processDocument(Document $document)
{
/** @var Element[] $tocs */
$tocs = [];
$headings = [];
$walker = $document->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if ($node instanceof Element && !$event->isEntering()) {
$tocs[] = $node;
continue;
}
if (!($node instanceof Heading) || !$event->isEntering()) {
continue;
}
$id = $this->addId($node);
$headings[] = new Entry($node, $id);
}
if (count($headings) && (count($tocs) || $this->hasAutoTOC())) {
$generated = $this->generate($headings);
if (count($tocs)) {
foreach ($tocs as $toc) {
$toc->replaceWith($this->render($generated->getChildren()));
}
} else {
$document->prependChild($this->render($generated->getChildren()));
}
}
}
protected function addId(Heading $node)
{
// If the node has an ID, no need to generate it
$attributes = $node->getData('attributes', []);
if (array_key_exists('id', $attributes) && !empty($attributes['id'])) {
// TODO :: check for uniqueness
return $attributes['id'];
}
// Well, seems we have to generate an ID
$walker = $node->walker();
$inside = [];
while ($event = $walker->next()) {
$insideNode = $event->getNode();
if ($insideNode instanceof Heading) {
continue;
}
$inside[] = $insideNode;
}
$text = '';
foreach ($inside as $other) {
if ($other instanceof Text) {
$text .= ' ' . $other->getContent();
}
}
$text = 'page_' . DauxHelper::slug(trim($text));
// TODO :: check for uniqueness
$node->data['attributes']['id'] = $text;
}
/**
* @param Entry[] $headings
* @return RootEntry
*/
public function generate($headings)
{
/** @var Entry $previous */
$root = $previous = new RootEntry();
foreach ($headings as $heading) {
if ($heading->getLevel() < $previous->getLevel()) {
$parent = $previous;
do {
$parent = $parent->getParent();
} while ($heading->getLevel() <= $parent->getLevel() || $parent->getLevel() != 0);
$parent->addChild($heading);
$previous = $heading;
continue;
}
if ($heading->getLevel() > $previous->getLevel()) {
$previous->addChild($heading);
$previous = $heading;
continue;
}
//if ($heading->getLevel() == $previous->getLevel()) {
$previous->getParent()->addChild($heading);
$previous = $heading;
continue;
//}
}
return $root;
}
/**
* @param Entry[] $entries
* @return ListBlock
*/
protected function render(array $entries)
{
$data = new ListData();
$data->type = ListBlock::TYPE_UNORDERED;
$list = new ListBlock($data);
$list->data['attributes']['class'] = 'TableOfContents';
foreach ($entries as $entry) {
$item = new ListItem($data);
$a = new Link('#' . $entry->getId());
foreach ($this->cloneChildren($entry->getContent()) as $node) {
$a->appendChild($node);
}
$p = new Paragraph();
$p->appendChild($a);
$item->appendChild($p);
if (!empty($entry->getChildren())) {
$item->appendChild($this->render($entry->getChildren()));
}
$list->appendChild($item);
}
return $list;
}
/**
* @param Heading $node
* @return Node[]
*/
protected function cloneChildren(Heading $node)
{
$deepCopy = new DeepCopy();
$firstClone = clone $node;
// We have no choice but to hack into the system to reset the parent, to avoid cloning the complete tree
$method = new ReflectionMethod(get_class($firstClone), 'setParent');
$method->setAccessible(true);
$method->invoke($firstClone, null);
return $deepCopy->copy($firstClone)->children();
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -375,3 +375,20 @@ table {
top: 10px;
}
}
.TableOfContents {
font-size:16px;
padding-left:30px;
border-left:6px solid #efefef;
p {
margin-bottom:0;
}
.TableOfContents {
border-left-width:0;
padding-left:20px;
}
}