Add Table Of Contents feature

Cette révision appartient à :
Stéphane Goetz 2016-04-12 08:38:52 +02:00
Parent 9a615ea55a
révision b037a43e2d
17 fichiers modifiés avec 496 ajouts et 7 suppressions

Voir le fichier

@ -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 générée
Voir le fichier

@ -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",

Voir le fichier

@ -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

Voir le fichier

@ -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

Voir le fichier

@ -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">

Voir le fichier

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

Voir le fichier

@ -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'])) {

Voir le fichier

@ -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;
}
}

Voir le fichier

@ -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();
}
}

Voir le fichier

@ -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;
}
}

Voir le fichier

@ -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");
}
}

Voir le fichier

@ -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();
}
}

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Diff de fichier supprimé car une ou plusieurs lignes sont trop longues

Voir le fichier

@ -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;
}
}