config = $config; } public function onDocumentParsed(DocumentParsedEvent $event) { $document = $event->getDocument(); /** @var TableOfContents[] $tocs */ $tocs = []; $headings = []; $document->heading_ids = []; $walker = $document->walker(); while ($event = $walker->next()) { $node = $event->getNode(); if ($node instanceof TableOfContents && !$event->isEntering()) { $tocs[] = $node; continue; } if (!($node instanceof Heading) || !$event->isEntering()) { continue; } $this->ensureHeadingHasId($document, $node); $headings[] = new Entry($node); } if (count($headings) && (count($tocs) || $this->config->getHTML()->hasAutomaticTableOfContents())) { $generated = $this->generate($headings); if (count($tocs)) { foreach ($tocs as $toc) { $toc->appendChild($this->render($generated->getChildren())); } } else { $document->prependChild($this->render($generated->getChildren())); } } } protected function getUniqueId(Document $document, $proposed) { if ($proposed == 'page_') { $proposed = 'page_section_' . (count($document->heading_ids) + 1); } // Quick path, it's a unique ID if (!in_array($proposed, $document->heading_ids)) { $document->heading_ids[] = $proposed; return $proposed; } $extension = 1; // Initialize the variable at one, so on the first iteration we have 2 do { ++$extension; } while (in_array("$proposed-$extension", $document->heading_ids)); return "$proposed-$extension"; } protected function ensureHeadingHasId(Document $document, Heading $node) { // If the node has an ID, no need to generate it, just check it's unique $attributes = $node->getData('attributes', []); if (array_key_exists('id', $attributes) && !empty($attributes['id'])) { $node->data['attributes']['id'] = $this->getUniqueId($document, $attributes['id']); return; } // 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(); } } $node->data['attributes']['id'] = $this->getUniqueId($document, 'page_' . DauxHelper::slug($text)); } /** * Make a tree of the list of headings. * * @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()); $content = $entry->getContent(); if ($content != null) { foreach ($this->cloneChildren($content) 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; } /** * Set the specified property to null on the object. * * @param Heading $object The object to modify * @param string $property The property to nullify */ protected function setNull(Heading $object, $property) { $prop = new \ReflectionProperty(get_class($object), $property); $prop->setAccessible(true); $prop->setValue($object, null); } /** * @return Node[] */ protected function cloneChildren(Heading $node) { $firstClone = clone $node; // We have no choice but to hack into the // system to reset the parent, previous and next $this->setNull($firstClone, 'parent'); $this->setNull($firstClone, 'previous'); $this->setNull($firstClone, 'next'); // Also, the child elements need to know the next parents foreach ($firstClone->children() as $subnode) { $method = new ReflectionMethod(get_class($subnode), 'setParent'); $method->setAccessible(true); $method->invoke($subnode, $firstClone); } return (new DeepCopy())->copy($firstClone)->children(); } }