Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 08:02:04 +01:00
parent 4413528994
commit 1d90fbf296
6865 changed files with 1091082 additions and 0 deletions

View file

@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use Doctrine\RST\Builder\Copier;
use Doctrine\RST\Builder\Documents;
use Doctrine\RST\Builder\ParseQueue;
use Doctrine\RST\Builder\ParseQueueProcessor;
use Doctrine\RST\Builder\Scanner;
use Doctrine\RST\Event\PostBuildRenderEvent;
use Doctrine\RST\Event\PreBuildParseEvent;
use Doctrine\RST\Event\PreBuildRenderEvent;
use Doctrine\RST\Event\PreBuildScanEvent;
use Doctrine\RST\Meta\CachedMetasLoader;
use Doctrine\RST\Meta\Metas;
use InvalidArgumentException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use function file_exists;
use function is_dir;
use function sprintf;
final class Builder
{
/** @var Kernel */
private $kernel;
/** @var Configuration */
private $configuration;
/** @var ErrorManager */
private $errorManager;
/** @var Filesystem */
private $filesystem;
/** @var Metas */
private $metas;
/** @var CachedMetasLoader */
private $cachedMetasLoader;
/** @var Documents */
private $documents;
/** @var Copier */
private $copier;
/** @var Finder|null */
private $scannerFinder;
/** @var string */
private $indexName = 'index';
public function __construct(?Kernel $kernel = null)
{
$this->kernel = $kernel ?? new Kernel();
$this->configuration = $this->kernel->getConfiguration();
$this->errorManager = new ErrorManager($this->configuration);
$this->filesystem = new Filesystem();
$this->metas = new Metas();
$this->cachedMetasLoader = new CachedMetasLoader();
$this->documents = new Builder\Documents(
$this->filesystem,
$this->metas
);
$this->copier = new Builder\Copier($this->filesystem);
$this->kernel->initBuilder($this);
}
public function recreate(): Builder
{
return new Builder($this->kernel);
}
public function getKernel(): Kernel
{
return $this->kernel;
}
public function getConfiguration(): Configuration
{
return $this->configuration;
}
public function getDocuments(): Documents
{
return $this->documents;
}
public function getErrorManager(): ErrorManager
{
return $this->errorManager;
}
public function setIndexName(string $name): self
{
$this->indexName = $name;
return $this;
}
/**
* Returns the "master document" name (e.g. index).
*
* This is the first document whose toctree should be read
* when building the table of contents. It is usually "index",
* which means index.rst is read first.
*/
public function getIndexName(): string
{
return $this->indexName;
}
public function getMetas(): Metas
{
return $this->metas;
}
public function build(
string $directory,
string $targetDirectory = 'output'
): void {
// Creating output directory if doesn't exists
if (! is_dir($targetDirectory)) {
$this->filesystem->mkdir($targetDirectory, 0755);
}
$indexFilename = sprintf('%s.%s', $this->indexName, $this->configuration->getSourceFileExtension());
if (! file_exists($directory . '/' . $indexFilename)) {
throw new InvalidArgumentException(sprintf('Could not find index file "%s" in "%s"', $indexFilename, $directory));
}
if ($this->configuration->getUseCachedMetas()) {
$this->cachedMetasLoader->loadCachedMetaEntries($targetDirectory, $this->metas);
}
$parseQueue = $this->scan($directory, $targetDirectory);
$this->parse($directory, $targetDirectory, $parseQueue);
$this->render($directory, $targetDirectory);
$this->cachedMetasLoader->cacheMetaEntries($targetDirectory, $this->metas);
}
public function copy(string $source, ?string $destination = null): self
{
$this->copier->copy($source, $destination);
return $this;
}
public function mkdir(string $directory): self
{
$this->copier->mkdir($directory);
return $this;
}
/**
* Set the Finder that will be used for scanning files.
*/
public function setScannerFinder(Finder $finder): void
{
$this->scannerFinder = $finder;
}
private function scan(string $directory, string $targetDirectory): ParseQueue
{
$this->configuration->dispatchEvent(
PreBuildScanEvent::PRE_BUILD_SCAN,
new PreBuildScanEvent($this, $directory, $targetDirectory)
);
$scanner = new Scanner(
$this->configuration->getSourceFileExtension(),
$directory,
$this->metas,
$this->getScannerFinder()
);
return $scanner->scan();
}
private function parse(string $directory, string $targetDirectory, ParseQueue $parseQueue): void
{
$this->configuration->dispatchEvent(
PreBuildParseEvent::PRE_BUILD_PARSE,
new PreBuildParseEvent($this, $directory, $targetDirectory, $parseQueue)
);
$parseQueueProcessor = new ParseQueueProcessor(
$this->kernel,
$this->errorManager,
$this->metas,
$this->documents,
$directory,
$targetDirectory,
$this->configuration->getFileExtension()
);
$parseQueueProcessor->process($parseQueue);
}
private function render(string $directory, string $targetDirectory): void
{
$this->configuration->dispatchEvent(
PreBuildRenderEvent::PRE_BUILD_RENDER,
new PreBuildRenderEvent($this, $directory, $targetDirectory)
);
$this->documents->render($targetDirectory);
$this->copier->doMkdir($targetDirectory);
$this->copier->doCopy($directory, $targetDirectory);
$this->configuration->dispatchEvent(
PostBuildRenderEvent::POST_BUILD_RENDER,
new PostBuildRenderEvent($this, $directory, $targetDirectory)
);
}
private function getScannerFinder(): Finder
{
if ($this->scannerFinder === null) {
$this->scannerFinder = new Finder();
}
return $this->scannerFinder;
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Builder;
use Symfony\Component\Filesystem\Filesystem;
use function basename;
use function dirname;
use function is_dir;
final class Copier
{
/** @var Filesystem */
private $filesystem;
/** @var string[][] */
private $toCopy = [];
/** @var string[] */
private $toMkdir = [];
public function __construct(Filesystem $filesystem)
{
$this->filesystem = $filesystem;
}
public function doCopy(string $sourceDirectory, string $targetDirectory): void
{
foreach ($this->toCopy as $copy) {
[$source, $destination] = $copy;
if ($source[0] !== '/') {
$source = $sourceDirectory . '/' . $source;
}
$destination = $targetDirectory . '/' . $destination;
if (is_dir($source) && is_dir($destination)) {
$destination = dirname($destination);
}
if (is_dir($source)) {
$this->filesystem->mirror($source, $destination);
} else {
$this->filesystem->copy($source, $destination);
}
}
$this->toCopy = [];
}
public function doMkdir(string $targetDirectory): void
{
foreach ($this->toMkdir as $mkdir) {
$dir = $targetDirectory . '/' . $mkdir;
if (is_dir($dir)) {
continue;
}
$this->filesystem->mkdir($dir, 0755);
}
$this->toMkdir = [];
}
public function copy(string $source, ?string $destination = null): void
{
if ($destination === null) {
$destination = basename($source);
}
$this->toCopy[] = [$source, $destination];
}
public function mkdir(string $directory): void
{
$this->toMkdir[] = $directory;
}
}

View file

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Builder;
use Doctrine\RST\Meta\Metas;
use Doctrine\RST\Nodes\DocumentNode;
use InvalidArgumentException;
use Symfony\Component\Filesystem\Filesystem;
use function dirname;
use function is_dir;
use function sprintf;
class Documents
{
/** @var Filesystem */
private $filesystem;
/** @var Metas */
private $metas;
/** @var DocumentNode[] */
private $documents = [];
public function __construct(
Filesystem $filesystem,
Metas $metas
) {
$this->filesystem = $filesystem;
$this->metas = $metas;
}
/** @return DocumentNode[] */
public function getAll(): array
{
return $this->documents;
}
public function hasDocument(string $file): bool
{
return isset($this->documents[$file]);
}
public function addDocument(string $file, DocumentNode $document): void
{
$this->documents[$file] = $document;
}
public function render(string $targetDirectory): void
{
foreach ($this->documents as $file => $document) {
$target = $this->getTargetOf($targetDirectory, $file);
$directory = dirname($target);
if (! is_dir($directory)) {
$this->filesystem->mkdir($directory, 0755);
}
$this->filesystem->dumpFile($target, $document->renderDocument());
}
}
private function getTargetOf(string $targetDirectory, string $file): string
{
$metaEntry = $this->metas->get($file);
if ($metaEntry === null) {
throw new InvalidArgumentException(sprintf('Could not find target file for %s', $file));
}
return $targetDirectory . '/' . $metaEntry->getUrl();
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Builder;
use InvalidArgumentException;
use function array_filter;
use function array_key_exists;
use function array_keys;
use function sprintf;
final class ParseQueue
{
/**
* An array where each key is the filename and the value is a
* boolean indicating if the file needs to be parsed or not.
*
* @var bool[]
*/
private $fileStatuses = [];
public function addFile(string $filename, bool $parseNeeded): void
{
if (isset($this->fileStatuses[$filename])) {
throw new InvalidArgumentException(sprintf('File "%s" is already in the parse queue', $filename));
}
$this->fileStatuses[$filename] = $parseNeeded;
}
public function isFileKnownToParseQueue(string $filename): bool
{
return array_key_exists($filename, $this->fileStatuses);
}
public function doesFileRequireParsing(string $filename): bool
{
if (! $this->isFileKnownToParseQueue($filename)) {
throw new InvalidArgumentException(sprintf('File "%s" is not known to the parse queue', $filename));
}
return $this->fileStatuses[$filename];
}
/** @return string[] */
public function getAllFilesThatRequireParsing(): array
{
return array_keys(array_filter($this->fileStatuses, static function (bool $parseNeeded): bool {
return $parseNeeded;
}));
}
}

View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Builder;
use Doctrine\RST\ErrorManager;
use Doctrine\RST\Kernel;
use Doctrine\RST\Meta\Metas;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Parser;
use function filemtime;
use function fwrite;
use function getenv;
use function sprintf;
use const PHP_SAPI;
use const STDERR;
final class ParseQueueProcessor
{
/** @var Kernel */
private $kernel;
/** @var ErrorManager */
private $errorManager;
/** @var Metas */
private $metas;
/** @var Documents */
private $documents;
/** @var string */
private $directory;
/** @var string */
private $targetDirectory;
/** @var string */
private $fileExtension;
public function __construct(
Kernel $kernel,
ErrorManager $errorManager,
Metas $metas,
Documents $documents,
string $directory,
string $targetDirectory,
string $fileExtension
) {
$this->kernel = $kernel;
$this->errorManager = $errorManager;
$this->metas = $metas;
$this->documents = $documents;
$this->directory = $directory;
$this->targetDirectory = $targetDirectory;
$this->fileExtension = $fileExtension;
}
public function process(ParseQueue $parseQueue): void
{
foreach ($parseQueue->getAllFilesThatRequireParsing() as $file) {
$this->processFile($file);
}
}
private function processFile(string $file): void
{
if (getenv('SHELL_VERBOSITY') >= 1 && PHP_SAPI === 'cli') {
fwrite(STDERR, sprintf("Processing file: %s\n", $file));
}
$fileAbsolutePath = $this->buildFileAbsolutePath($file);
$parser = $this->createFileParser($file);
$environment = $parser->getEnvironment();
$document = $parser->parseFile($fileAbsolutePath);
$this->documents->addDocument($file, $document);
$this->kernel->postParse($document);
$this->metas->set(
$file,
$this->buildDocumentUrl($document),
(string) $document->getTitle(),
$document->getTitles(),
$document->getTocs(),
(int) filemtime($fileAbsolutePath),
$environment->getDependencies(),
$environment->getLinks()
);
}
private function createFileParser(string $file): Parser
{
$parser = new Parser($this->kernel);
$environment = $parser->getEnvironment();
$environment->setMetas($this->metas);
$environment->setCurrentFileName($file);
$environment->setCurrentDirectory($this->directory);
$environment->setTargetDirectory($this->targetDirectory);
$environment->setErrorManager($this->errorManager);
return $parser;
}
private function buildFileAbsolutePath(string $file): string
{
return $this->directory . '/' . $file . '.rst';
}
private function buildDocumentUrl(DocumentNode $document): string
{
return $document->getEnvironment()->getUrl() . '.' . $this->fileExtension;
}
}

View file

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Builder;
use Doctrine\RST\Meta\Metas;
use InvalidArgumentException;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use function sprintf;
use function strlen;
use function substr;
final class Scanner
{
/** @var string */
private $fileExtension;
/** @var string */
private $directory;
/** @var Metas */
private $metas;
/** @var Finder */
private $finder;
/** @var SplFileInfo[] */
private $fileInfos = [];
public function __construct(string $fileExtension, string $directory, Metas $metas, ?Finder $finder = null)
{
$this->fileExtension = $fileExtension;
$this->directory = $directory;
$this->metas = $metas;
$this->finder = $finder ?? new Finder();
$this->finder->in($this->directory)
->files()
->name('*.' . $this->fileExtension);
}
/**
* Scans a directory recursively looking for all files to parse.
*
* This takes into account the presence of cached & fresh MetaEntry
* objects, and avoids adding files to the parse queue that have
* not changed and whose direct dependencies have not changed.
*/
public function scan(): ParseQueue
{
// completely populate the splFileInfos property
$this->fileInfos = [];
foreach ($this->finder as $fileInfo) {
$relativeFilename = $fileInfo->getRelativePathname();
// strip off the extension
$documentPath = substr($relativeFilename, 0, -(strlen($this->fileExtension) + 1));
$this->fileInfos[$documentPath] = $fileInfo;
}
$parseQueue = new ParseQueue();
foreach ($this->fileInfos as $filename => $fileInfo) {
$parseQueue->addFile(
$filename,
$this->doesFileRequireParsing($filename, $parseQueue)
);
}
return $parseQueue;
}
private function doesFileRequireParsing(string $filename, ParseQueue $parseQueue): bool
{
if (! isset($this->fileInfos[$filename])) {
throw new InvalidArgumentException(sprintf('No file info found for "%s" - file does not exist.', $filename));
}
$file = $this->fileInfos[$filename];
$documentFilename = $this->getFilenameFromFile($file);
$entry = $this->metas->get($documentFilename);
if ($this->hasFileBeenUpdated($filename)) {
// File is new or changed and thus need to be parsed
return true;
}
// Look to the file's dependencies to know if you need to parse it or not
$dependencies = $entry !== null ? $entry->getDepends() : [];
if ($entry !== null && $entry->getParent() !== null) {
$dependencies[] = $entry->getParent();
}
foreach ($dependencies as $dependency) {
/*
* The dependency check is NOT recursive on purpose.
* If fileA has a link to fileB that uses its "headline",
* for example, then fileA is "dependent" on fileB. If
* fileB changes, it means that its MetaEntry needs to
* be updated. And because fileA gets the headline from
* the MetaEntry, it means that fileA must also be re-parsed.
* However, if fileB depends on fileC and file C only is
* updated, fileB *does* need to be re-parsed, but fileA
* does not, because the MetaEntry for fileB IS still
* "fresh" - fileB did not actually change, so any metadata
* about headlines, etc, is still fresh. Therefore, fileA
* does not need to be parsed.
*/
// dependency no longer exists? We should re-parse this file
if (! isset($this->fileInfos[$dependency])) {
return true;
}
// finally, we need to recursively ask if this file needs parsing
if ($this->hasFileBeenUpdated($dependency)) {
return true;
}
}
// Meta is fresh and no dependencies need parsing
return false;
}
private function hasFileBeenUpdated(string $filename): bool
{
$file = $this->fileInfos[$filename];
$documentFilename = $this->getFilenameFromFile($file);
$entry = $this->metas->get($documentFilename);
// File is new or changed
return $entry === null || $entry->getMtime() < $file->getMTime();
}
/**
* Converts foo/bar.rst to foo/bar (the document filename)
*/
private function getFilenameFromFile(SplFileInfo $file): string
{
return substr($file->getRelativePathname(), 0, -(strlen($this->fileExtension) + 1));
}
}

View file

@ -0,0 +1,381 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use Doctrine\Common\EventArgs;
use Doctrine\Common\EventManager;
use Doctrine\RST\Formats\Format;
use Doctrine\RST\Formats\InternalFormat;
use Doctrine\RST\HTML\HTMLFormat;
use Doctrine\RST\LaTeX\LaTeXFormat;
use Doctrine\RST\NodeFactory\DefaultNodeFactory;
use Doctrine\RST\NodeFactory\NodeFactory;
use Doctrine\RST\NodeFactory\NodeInstantiator;
use Doctrine\RST\Nodes\NodeTypes;
use Doctrine\RST\Renderers\NodeRendererFactory;
use Doctrine\RST\Templates\TemplateEngineAdapter;
use Doctrine\RST\Templates\TemplateRenderer;
use Doctrine\RST\Templates\TwigAdapter;
use Doctrine\RST\Templates\TwigTemplateRenderer;
use RuntimeException;
use Twig\Environment as TwigEnvironment;
use function sprintf;
use function sys_get_temp_dir;
class Configuration
{
public const THEME_DEFAULT = 'default';
public const OUTPUT_FORMAT_CONSOLE = 'console';
public const OUTPUT_FORMAT_GITHUB = 'github';
/** @var string */
private $cacheDir;
/** @var string[] */
private $customTemplateDirs = [];
/** @var string */
private $theme = self::THEME_DEFAULT;
/** @var string */
private $baseUrl = '';
/** @var callable|null */
private $baseUrlEnabledCallable;
/** @var bool */
private $abortOnError = true;
/** @var bool */
private $silentOnError = false;
/** @var bool */
private $warningsAsError = false;
/** @psalm-var self::OUTPUT_FORMAT_* */
private $outputFormat = self::OUTPUT_FORMAT_CONSOLE;
/** @var bool */
private $ignoreInvalidReferences = false;
/** @var bool */
private $indentHTML = false;
/** @var int */
private $initialHeaderLevel = 1;
/** @var bool */
private $useCachedMetas = true;
/** @var string */
private $fileExtension = Format::HTML;
/** @var string */
private $sourceFileExtension = 'rst';
/** @var TemplateRenderer */
private $templateRenderer;
/** @var Format[] */
private $formats;
/** @var NodeFactory|null */
private $nodeFactory;
/** @var EventManager */
private $eventManager;
/** @var TemplateEngineAdapter */
private $templateEngineAdapter;
public function __construct()
{
$this->cacheDir = sys_get_temp_dir() . '/doctrine-rst-parser';
$this->eventManager = new EventManager();
$this->templateEngineAdapter = new TwigAdapter($this);
$this->templateRenderer = new TwigTemplateRenderer($this);
$this->formats = [
Format::HTML => new InternalFormat(new HTMLFormat($this->templateRenderer)),
Format::LATEX => new InternalFormat(new LaTeXFormat($this->templateRenderer)),
];
}
public function getCacheDir(): string
{
return $this->cacheDir;
}
public function setCacheDir(string $cacheDir): void
{
$this->cacheDir = $cacheDir;
}
public function getTemplateRenderer(): TemplateRenderer
{
return $this->templateRenderer;
}
public function setTemplateRenderer(TemplateRenderer $templateRenderer): void
{
$this->templateRenderer = $templateRenderer;
}
/** @return mixed|TwigEnvironment */
public function getTemplateEngine()
{
return $this->templateEngineAdapter->getTemplateEngine();
}
/** @return string[] */
public function getCustomTemplateDirs(): array
{
return $this->customTemplateDirs;
}
/** @param string[] $customTemplateDirs */
public function setCustomTemplateDirs(array $customTemplateDirs): void
{
$this->customTemplateDirs = $customTemplateDirs;
}
public function addCustomTemplateDir(string $customTemplateDir): void
{
$this->customTemplateDirs[] = $customTemplateDir;
}
public function getTheme(): string
{
return $this->theme;
}
public function setTheme(string $theme): void
{
$this->theme = $theme;
}
public function getBaseUrl(): string
{
return $this->baseUrl;
}
public function setBaseUrl(string $baseUrl): self
{
$this->baseUrl = $baseUrl;
return $this;
}
public function setBaseUrlEnabledCallable(?callable $baseUrlEnabledCallable): void
{
$this->baseUrlEnabledCallable = $baseUrlEnabledCallable;
}
public function getBaseUrlEnabledCallable(): ?callable
{
return $this->baseUrlEnabledCallable;
}
public function isBaseUrlEnabled(string $path): bool
{
if ($this->baseUrl === '') {
return false;
}
if ($this->baseUrlEnabledCallable !== null) {
$baseUrlEnabledCallable = $this->baseUrlEnabledCallable;
return $baseUrlEnabledCallable($path);
}
return true;
}
public function isAbortOnError(): bool
{
return $this->abortOnError;
}
public function abortOnError(bool $abortOnError): void
{
$this->abortOnError = $abortOnError;
}
public function silentOnError(bool $silentOnError = true): void
{
$this->silentOnError = $silentOnError;
}
public function isSilentOnError(): bool
{
return $this->silentOnError;
}
public function treatWarningsAsError(bool $warningsAsError): void
{
$this->warningsAsError = $warningsAsError;
}
public function isWarningsAsError(): bool
{
return $this->warningsAsError;
}
/** @psalm-return self::OUTPUT_FORMAT_* */
final public function getOutputFormat(): string
{
return $this->outputFormat;
}
/** @psalm-param self::OUTPUT_FORMAT_* $outputFormat */
final public function setOutputFormat(string $outputFormat): void
{
$this->outputFormat = $outputFormat;
}
public function getIgnoreInvalidReferences(): bool
{
return $this->ignoreInvalidReferences;
}
public function setIgnoreInvalidReferences(bool $ignoreInvalidReferences): void
{
$this->ignoreInvalidReferences = $ignoreInvalidReferences;
}
public function setIndentHTML(bool $indentHTML): void
{
$this->indentHTML = $indentHTML;
}
public function getIndentHTML(): bool
{
return $this->indentHTML;
}
public function setInitialHeaderLevel(int $initialHeaderLevel): void
{
$this->initialHeaderLevel = $initialHeaderLevel;
}
public function getInitialHeaderLevel(): int
{
return $this->initialHeaderLevel;
}
public function setUseCachedMetas(bool $useCachedMetas): void
{
$this->useCachedMetas = $useCachedMetas;
}
public function getUseCachedMetas(): bool
{
return $this->useCachedMetas;
}
public function getFileExtension(): string
{
return $this->fileExtension;
}
public function setFileExtension(string $fileExtension): void
{
$this->fileExtension = $fileExtension;
}
public function getNodeFactory(Environment $environment): NodeFactory
{
if ($this->nodeFactory !== null) {
return $this->nodeFactory;
}
return new DefaultNodeFactory(
$this->eventManager,
$this->createNodeInstantiator($environment, NodeTypes::DOCUMENT, Nodes\DocumentNode::class),
$this->createNodeInstantiator($environment, NodeTypes::SPAN, Nodes\SpanNode::class),
$this->createNodeInstantiator($environment, NodeTypes::TOC, Nodes\TocNode::class),
$this->createNodeInstantiator($environment, NodeTypes::TITLE, Nodes\TitleNode::class),
$this->createNodeInstantiator($environment, NodeTypes::SEPARATOR, Nodes\SeparatorNode::class),
$this->createNodeInstantiator($environment, NodeTypes::CODE, Nodes\CodeNode::class),
$this->createNodeInstantiator($environment, NodeTypes::QUOTE, Nodes\QuoteNode::class),
$this->createNodeInstantiator($environment, NodeTypes::PARAGRAPH, Nodes\ParagraphNode::class),
$this->createNodeInstantiator($environment, NodeTypes::ANCHOR, Nodes\AnchorNode::class),
$this->createNodeInstantiator($environment, NodeTypes::LIST, Nodes\ListNode::class),
$this->createNodeInstantiator($environment, NodeTypes::TABLE, Nodes\TableNode::class),
$this->createNodeInstantiator($environment, NodeTypes::DEFINITION_LIST, Nodes\DefinitionListNode::class),
$this->createNodeInstantiator($environment, NodeTypes::WRAPPER, Nodes\WrapperNode::class),
$this->createNodeInstantiator($environment, NodeTypes::FIGURE, Nodes\FigureNode::class),
$this->createNodeInstantiator($environment, NodeTypes::IMAGE, Nodes\ImageNode::class),
$this->createNodeInstantiator($environment, NodeTypes::META, Nodes\MetaNode::class),
$this->createNodeInstantiator($environment, NodeTypes::RAW, Nodes\RawNode::class),
$this->createNodeInstantiator($environment, NodeTypes::DUMMY, Nodes\DummyNode::class),
$this->createNodeInstantiator($environment, NodeTypes::MAIN, Nodes\MainNode::class),
$this->createNodeInstantiator($environment, NodeTypes::BLOCK, Nodes\BlockNode::class),
$this->createNodeInstantiator($environment, NodeTypes::CALLABLE, Nodes\CallableNode::class),
$this->createNodeInstantiator($environment, NodeTypes::SECTION_BEGIN, Nodes\SectionBeginNode::class),
$this->createNodeInstantiator($environment, NodeTypes::SECTION_END, Nodes\SectionEndNode::class)
);
}
public function setNodeFactory(NodeFactory $nodeFactory): void
{
$this->nodeFactory = $nodeFactory;
}
public function setEventManager(EventManager $eventManager): void
{
$this->eventManager = $eventManager;
}
public function getEventManager(): EventManager
{
return $this->eventManager;
}
public function dispatchEvent(string $eventName, ?EventArgs $eventArgs = null): void
{
$this->eventManager->dispatchEvent($eventName, $eventArgs);
}
public function addFormat(Format $format): void
{
$this->formats[$format->getFileExtension()] = $format;
}
public function getFormat(): Format
{
if (! isset($this->formats[$this->fileExtension])) {
throw new RuntimeException(
sprintf('Format %s does not exist.', $this->fileExtension)
);
}
return $this->formats[$this->fileExtension];
}
public function getSourceFileExtension(): string
{
return $this->sourceFileExtension;
}
private function createNodeInstantiator(Environment $environment, string $type, string $nodeClassName): NodeInstantiator
{
return new NodeInstantiator(
$type,
$nodeClassName,
$environment,
$this->getNodeRendererFactory($nodeClassName),
$this->eventManager
);
}
private function getNodeRendererFactory(string $nodeClassName): ?NodeRendererFactory
{
return $this->getFormat()->getNodeRendererFactories()[$nodeClassName] ?? null;
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Nodes\WrapperNode;
use Doctrine\RST\Parser;
use function sprintf;
class Admonition extends SubDirective
{
/** @var string */
private $name;
/** @var string */
private $backgroundColor;
/** @var string */
private $textColor;
/** @var string */
private $icon;
public function __construct(string $name, string $backgroundColor, string $textColor, string $icon)
{
$this->name = $name;
$this->backgroundColor = $backgroundColor;
$this->textColor = $textColor;
$this->icon = $icon;
}
public function getName(): string
{
return $this->name;
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
return new WrapperNode($document, sprintf(
'<div class="alert %s-admonition %s %s border"><table width="100%%"><tr><td width="10" class="align-top"><i class="%s mr-2"></i></td><td>',
$this->name,
$this->backgroundColor,
$this->textColor,
$this->icon
), '</td></tr></table></div>');
}
}

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use function trim;
/**
* Renders a code block, example:
*
* .. code-block:: php
*
* <?php
*
* echo "Hello world!\n";
*/
final class CodeBlock extends Directive
{
public function getName(): string
{
return 'code-block';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
if ($node === null) {
return;
}
$kernel = $parser->getKernel();
if ($node instanceof CodeNode) {
$node->setLanguage(trim($data));
$node->setOptions($options);
}
if ($variable !== '') {
$environment = $parser->getEnvironment();
$environment->setVariable($variable, $node);
} else {
$document = $parser->getDocument();
$document->addNode($node);
}
}
public function wantCode(): bool
{
return true;
}
}

View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* A directive is like a function you can call or apply to a block
* Il looks like:
*
* .. function:: main
* :arg1: value
* :arg2: otherValue
*
* Some block !
*
* The directive can define variables, create special nodes or change
* the node that directly follows it
*/
abstract class Directive
{
/**
* Get the directive name
*/
abstract public function getName(): string;
/**
* This is the function called by the parser to process the directive, it can be overloaded
* to do anything with the document, like tweaking nodes or change the environment
*
* The node that directly follows the directive is also passed to it
*
* @param Parser $parser the calling parser
* @param Node|null $node the node that follows the directive
* @param string $variable the variable name of the directive
* @param string $data the data of the directive (following ::)
* @param string[] $options the array of options for this directive
*/
public function process(Parser $parser, ?Node $node, string $variable, string $data, array $options): void
{
$document = $parser->getDocument();
$processNode = $this->processNode($parser, $variable, $data, $options);
if ($processNode !== null) {
if ($variable !== '') {
$environment = $parser->getEnvironment();
$environment->setVariable($variable, $processNode);
} else {
$document->addNode($processNode);
}
}
if ($node === null) {
return;
}
$document->addNode($node);
}
/**
* This can be overloaded to write a directive that just create one node for the
* document, which is common
*
* The arguments are the same that process
*
* @param string[] $options
*/
public function processNode(Parser $parser, string $variable, string $data, array $options): ?Node
{
$this->processAction($parser, $variable, $data, $options);
return null;
}
/**
* This can be overloaded to write a directive that just do an action without changing
* the nodes of the document
*
* The arguments are the same that process
*
* @param string[] $options
*/
public function processAction(Parser $parser, string $variable, string $data, array $options): void
{
}
/**
* Called at the end of the parsing to finalize the document (add something or tweak nodes)
*/
public function finalize(DocumentNode $document): void
{
}
/**
* Should the following block be passed as a CodeNode?
*
* You should probably return false from this. If you do,
* in most cases (unless you directive allows for some fancy
* syntax), you will receive a BlockNode object in processNode().
*
* @see CodeNode
*/
public function wantCode(): bool
{
return false;
}
/**
* Can this directive apply to content that is not indented under it?
*
* Most directives that allow content require that content to be
* indented under it. For example:
*
* .. note::
*
* This is my note! It must be indented.
*
* But some are allowed to apply to content that is *not* indented:
*
* .. class:: align-center
*
* I will be a "p" tag with an align-center class
*
* If your directive allows the "class" directive functionality,
* return true from this function. The result is that your
* directive's process() method will be called for the next
* node after your directive (e.g. a ParagraphNode, ListNode, etc)
*/
public function appliesToNonBlockContent(): bool
{
return false;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
final class Dummy extends Directive
{
public function getName(): string
{
return 'dummy';
}
/** @param string[] $options */
public function processNode(
Parser $parser,
string $variable,
string $data,
array $options
): ?Node {
return $parser->getNodeFactory()->createDummyNode([
'data' => $data,
'options' => $options,
]);
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Renders a raw block, example:
*
* .. raw::
*
* <u>Undelined!</u>
*/
final class Raw extends Directive
{
public function getName(): string
{
return 'raw';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
if ($node === null) {
return;
}
$kernel = $parser->getKernel();
if ($node instanceof CodeNode) {
$node->setRaw(true);
}
if ($variable !== '') {
$environment = $parser->getEnvironment();
$environment->setVariable($variable, $node);
} else {
$document = $parser->getDocument();
$document->addNode($node);
}
}
public function wantCode(): bool
{
return true;
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* The Replace directive will set the variables for the spans
*
* .. |test| replace:: The Test String!
*/
final class Replace extends Directive
{
public function getName(): string
{
return 'replace';
}
/** @param string[] $options */
public function processNode(
Parser $parser,
string $variable,
string $data,
array $options
): ?Node {
return $parser->createSpanNode($data);
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\BlockNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* A directive that parses the sub block and call the processSub that can
* be overloaded, like :
*
* .. sub-directive::
* Some block of code
*
* You can imagine anything here, like adding *emphasis*, lists or
* titles
*/
abstract class SubDirective extends Directive
{
/** @param string[] $options */
final public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
/*
* A BlockNode indicates that the content was passed in "raw" and should
* be sub-parsed. This is the main use-case.
*
* However, there are some odd cases where a directive appears
* to "end"... and DocumentParser continues parsing the next
* section, for example as a ParagraphNode or ListNode. When
* that terminates, the original directive is THEN processed
* and called.
*
* A key example is the "class::" directive (see class-directive.rst test case).
* That is where it is legal to have a format like this:
*
* .. class:: special-list
*
* - Test list item 1.
* - Test list item 2.
*
* Notice the 2 list items are NOT indented. This is legal, and ultimately
* those two items would be parsed as a ListNode and THEN passed to
* ClassDirective (which extends SubDirective) for processing.
*/
if ($node instanceof BlockNode) {
$document = $parser->getSubParser()->parseLocal($node->getValue());
} else {
// If the $node is null, it represents a node with no content.
// Some directives - like "figure" - both allow content AND no content.
$document = $node;
}
$newNode = $this->processSub($parser, $document, $variable, $data, $options);
if ($newNode === null) {
return;
}
if ($variable !== '') {
$parser->getEnvironment()->setVariable($variable, $newNode);
} else {
$parser->getDocument()->addNode($newNode);
}
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
return null;
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
class Tip extends Admonition
{
public function __construct()
{
parent::__construct('tip', 'bg-success', 'text-light', 'fas fa-question-circle');
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Directives;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use Doctrine\RST\Toc\GlobSearcher;
use Doctrine\RST\Toc\ToctreeBuilder;
final class Toctree extends Directive
{
/** @var ToctreeBuilder */
private $toctreeBuilder;
public function __construct()
{
$this->toctreeBuilder = new ToctreeBuilder(new GlobSearcher());
}
public function getName(): string
{
return 'toctree';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
if ($node === null) {
return;
}
$environment = $parser->getEnvironment();
$toctreeFiles = $this->toctreeBuilder
->buildToctreeFiles($environment, $node, $options);
foreach ($toctreeFiles as $file) {
$environment->addDependency($file);
}
$tocNode = $parser->getNodeFactory()
->createTocNode($environment, $toctreeFiles, $options);
$parser->getDocument()->addNode($tocNode);
}
}

View file

@ -0,0 +1,474 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use Doctrine\RST\Meta\MetaEntry;
use Doctrine\RST\Meta\Metas;
use Doctrine\RST\NodeFactory\NodeFactory;
use Doctrine\RST\References\Reference;
use Doctrine\RST\References\ResolvedReference;
use Doctrine\RST\Templates\TemplateRenderer;
use InvalidArgumentException;
use Symfony\Component\String\Slugger\AsciiSlugger;
use function array_shift;
use function dirname;
use function implode;
use function in_array;
use function sprintf;
use function strtolower;
use function trim;
class Environment
{
/** @var Configuration */
private $configuration;
/** @var ErrorManager */
private $errorManager;
/** @var UrlGenerator */
private $urlGenerator;
/** @var int */
private $currentTitleLevel = 0;
/** @var string[] */
private $titleLetters = [];
/** @var string */
private $currentFileName = '';
/** @var string */
private $currentDirectory = '.';
/** @var string */
private $targetDirectory = '.';
/** @var string|null */
private $url = null;
/** @var Reference[] */
private $references = [];
/** @var Metas */
private $metas;
/** @var string[] */
private $dependencies = [];
/** @var string[] */
private $unresolvedDependencies = [];
/** @var string[] */
private $originalDependencyNames = [];
/** @var string[] */
private $variables = [];
/** @var string[] */
private $links = [];
/** @var int[] */
private $levels = [];
/** @var int[] */
private $counters = [];
/** @var string[] */
private $anonymous = [];
/** @var InvalidLink[] */
private $invalidLinks = [];
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
$this->errorManager = new ErrorManager($this->configuration);
$this->urlGenerator = new UrlGenerator(
$this->configuration
);
$this->metas = new Metas();
$this->reset();
}
public function reset(): void
{
$this->titleLetters = [];
$this->currentTitleLevel = 0;
$this->levels = [];
$this->counters = [];
for ($level = 0; $level < 16; $level++) {
$this->levels[$level] = 1;
$this->counters[$level] = 0;
}
}
public function getConfiguration(): Configuration
{
return $this->configuration;
}
public function getErrorManager(): ErrorManager
{
return $this->errorManager;
}
public function setErrorManager(ErrorManager $errorManager): void
{
$this->errorManager = $errorManager;
}
public function setMetas(Metas $metas): void
{
$this->metas = $metas;
}
public function getNodeFactory(): NodeFactory
{
return $this->configuration->getNodeFactory($this);
}
public function getTemplateRenderer(): TemplateRenderer
{
return $this->configuration->getTemplateRenderer();
}
public function registerReference(Reference $reference): void
{
$this->references[$reference->getName()] = $reference;
}
public function resolve(string $section, string $data): ?ResolvedReference
{
if (! isset($this->references[$section])) {
$this->addMissingReferenceSectionError($section);
return null;
}
$reference = $this->references[$section];
$resolvedReference = $reference->resolve($this, $data);
if ($resolvedReference === null) {
$this->addInvalidLink(new InvalidLink($data));
if ($this->getMetaEntry() !== null) {
$this->getMetaEntry()->removeDependency(
// use the original name
$this->originalDependencyNames[$data] ?? $data
);
}
return null;
}
if (isset($this->unresolvedDependencies[$data]) && $this->getMetaEntry() !== null) {
$this->getMetaEntry()->resolveDependency(
// use the unique, unresolved name
$this->unresolvedDependencies[$data],
$resolvedReference->getFile()
);
}
return $resolvedReference;
}
public function addInvalidLink(InvalidLink $invalidLink): void
{
$this->invalidLinks[] = $invalidLink;
}
/** @return InvalidLink[] */
public function getInvalidLinks(): array
{
return $this->invalidLinks;
}
/** @return string[]|null */
public function found(string $section, string $data): ?array
{
if (isset($this->references[$section])) {
$reference = $this->references[$section];
$reference->found($this, $data);
return null;
}
$this->addMissingReferenceSectionError($section);
return null;
}
/** @param mixed $value */
public function setVariable(string $variable, $value): void
{
$this->variables[$variable] = $value;
}
public function createTitle(int $level): string
{
for ($currentLevel = 0; $currentLevel < 16; $currentLevel++) {
if ($currentLevel <= $level) {
continue;
}
$this->levels[$currentLevel] = 1;
$this->counters[$currentLevel] = 0;
}
$this->levels[$level] = 1;
$this->counters[$level]++;
$token = ['title'];
for ($i = 1; $i <= $level; $i++) {
$token[] = $this->counters[$i];
}
return implode('.', $token);
}
public function getNumber(int $level): int
{
return $this->levels[$level]++;
}
/**
* @param mixed|null $default
*
* @return mixed
*/
public function getVariable(string $variable, $default = null)
{
if (isset($this->variables[$variable])) {
return $this->variables[$variable];
}
return $default;
}
public function setLink(string $name, string $url): void
{
$name = trim(strtolower($name));
if ($name === '_') {
$name = array_shift($this->anonymous);
}
$this->links[$name] = trim($url);
}
public function resetAnonymousStack(): void
{
$this->anonymous = [];
}
public function pushAnonymous(string $name): void
{
$this->anonymous[] = trim(strtolower($name));
}
/** @return string[] */
public function getLinks(): array
{
return $this->links;
}
public function getLink(string $name, bool $relative = true): string
{
$name = trim(strtolower($name));
if (isset($this->links[$name])) {
$link = $this->links[$name];
if ($relative) {
return (string) $this->relativeUrl($link);
}
return $link;
}
return '';
}
public function addDependency(string $dependency, bool $requiresResolving = false): void
{
if ($requiresResolving) {
// a hack to avoid collisions between resolved and unresolved dependencies
$dependencyName = 'UNRESOLVED__' . $dependency;
$this->unresolvedDependencies[$dependency] = $dependencyName;
// map the original dependency name to the one that will be stored
$this->originalDependencyNames[$dependency] = $dependencyName;
} else {
// the dependency is already a filename, probably a :doc:
// or from a toc-tree - change it to the canonical URL
$canonicalDependency = $this->canonicalUrl($dependency);
if ($canonicalDependency === null) {
throw new InvalidArgumentException(sprintf(
'Could not get canonical url for dependency %s',
$dependency
));
}
$dependencyName = $canonicalDependency;
// map the original dependency name to the one that will be stored
$this->originalDependencyNames[$dependency] = $canonicalDependency;
}
if (in_array($dependencyName, $this->dependencies, true)) {
return;
}
$this->dependencies[] = $dependencyName;
}
/** @return string[] */
public function getDependencies(): array
{
return $this->dependencies;
}
public function relativeUrl(?string $url): ?string
{
return $this->urlGenerator->relativeUrl($url, $this->currentFileName);
}
public function absoluteUrl(string $url): string
{
return $this->urlGenerator->absoluteUrl($this->getDirName(), $url);
}
public function canonicalUrl(string $url): ?string
{
return $this->urlGenerator->canonicalUrl($this->getDirName(), $url);
}
public function generateUrl(string $path): string
{
return $this->urlGenerator->generateUrl(
$path,
$this->currentFileName,
$this->getDirName()
);
}
public function getDirName(): string
{
$dirname = dirname($this->currentFileName);
if ($dirname === '.') {
return '';
}
return $dirname;
}
public function setCurrentFileName(string $filename): void
{
$this->currentFileName = $filename;
}
/**
* Returns the currently-parsed filename.
*
* This is relative to the root source directory and without
* the extension (e.g. "index" or "subdir/file")
*/
public function getCurrentFileName(): string
{
return $this->currentFileName;
}
public function setCurrentDirectory(string $directory): void
{
$this->currentDirectory = $directory;
}
public function getCurrentDirectory(): string
{
return $this->currentDirectory;
}
public function absoluteRelativePath(string $url): string
{
return $this->currentDirectory . '/' . $this->getDirName() . '/' . $this->relativeUrl($url);
}
public function setTargetDirectory(string $directory): void
{
$this->targetDirectory = $directory;
}
public function getTargetDirectory(): string
{
return $this->targetDirectory;
}
public function getUrl(): string
{
if ($this->url !== null) {
return $this->url;
}
return $this->currentFileName;
}
public function setUrl(string $url): void
{
if ($this->getDirName() !== '') {
$url = $this->getDirName() . '/' . $url;
}
$this->url = $url;
}
public function getMetas(): Metas
{
return $this->metas;
}
public function getMetaEntry(): ?MetaEntry
{
return $this->metas->get($this->currentFileName);
}
public function getLevel(string $letter): int
{
foreach ($this->titleLetters as $level => $titleLetter) {
if ($letter === $titleLetter) {
return $level;
}
}
$this->currentTitleLevel++;
$this->titleLetters[$this->currentTitleLevel] = $letter;
return $this->currentTitleLevel;
}
/** @return string[] */
public function getTitleLetters(): array
{
return $this->titleLetters;
}
public static function slugify(string $text): string
{
return (new AsciiSlugger('en', []))->slug($text)->lower()->toString();
}
private function addMissingReferenceSectionError(string $section): void
{
$this->errorManager->error(
sprintf('Unknown reference section "%s"', $section),
$this->getCurrentFileName()
);
}
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use Throwable;
use function sprintf;
final class Error
{
/** @var string */
private $message;
/** @var string|null */
private $file;
/** @var int|null */
private $line;
/** @var Throwable|null */
private $throwable;
public function __construct(string $message, ?string $file = null, ?int $line = null, ?Throwable $throwable = null)
{
$this->message = $message;
$this->file = $file;
$this->line = $line;
$this->throwable = $throwable;
}
public function asString(): string
{
$output = $this->message;
if ($this->getFile() !== null) {
$output .= sprintf(' in file "%s"', $this->file);
if ($this->line !== null) {
$output .= sprintf(' at line %d', $this->line);
}
}
return $output;
}
public function getMessage(): string
{
return $this->message;
}
public function getFile(): ?string
{
if ($this->file === '') {
return null;
}
return $this->file;
}
public function getLine(): ?int
{
return $this->line;
}
public function getThrowable(): ?Throwable
{
return $this->throwable;
}
}

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use Exception;
use Throwable;
use function sprintf;
class ErrorManager
{
/** @var Configuration */
private $configuration;
/** @var list<Error> */
private $errors = [];
public function __construct(Configuration $configuration)
{
$this->configuration = $configuration;
}
public function error(string $message, ?string $file = null, ?int $line = null, ?Throwable $throwable = null): void
{
$this->errors[] = $error = new Error($message, $file, $line, $throwable);
if (! $this->configuration->isSilentOnError()) {
if ($this->configuration->getOutputFormat() === Configuration::OUTPUT_FORMAT_GITHUB) {
$file = $error->getFile();
echo sprintf(
'::error %s%s::%s',
$file !== null ? 'file=' . $file : '',
$file !== null && $error->getLine() !== null ? ',linefile=' . $error->getLine() : '',
$error->getMessage()
);
} else {
echo '⚠️ ' . $error->asString() . "\n";
}
}
if ($this->configuration->isAbortOnError()) {
throw new Exception($error->asString(), 0, $error->getThrowable());
}
}
public function warning(string $message, ?string $file = null, ?int $line = null, ?Throwable $throwable = null): void
{
if ($this->configuration->isWarningsAsError()) {
$this->error($message, $file, $line, $throwable);
return;
}
if ($this->configuration->isSilentOnError()) {
return;
}
$error = new Error($message, $file, $line, $throwable);
if ($this->configuration->getOutputFormat() === Configuration::OUTPUT_FORMAT_GITHUB) {
$file = $error->getFile();
echo sprintf(
'::warning %s%s::%s',
$file !== null ? 'file=' . $file : '',
$file !== null && $error->getLine() !== null ? ',linefile=' . $error->getLine() : '',
$error->getMessage()
);
} else {
echo $error->asString() . "\n";
}
}
/** @return list<Error> */
public function getErrors(): array
{
return $this->errors;
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
use Doctrine\RST\Builder;
abstract class BuildEvent extends EventArgs
{
/** @var Builder */
private $builder;
/** @var string */
private $directory;
/** @var string */
private $targetDirectory;
public function __construct(
Builder $builder,
string $directory,
string $targetDirectory
) {
$this->builder = $builder;
$this->directory = $directory;
$this->targetDirectory = $targetDirectory;
}
public function getBuilder(): Builder
{
return $this->builder;
}
public function getDirectory(): string
{
return $this->directory;
}
public function getTargetDirectory(): string
{
return $this->targetDirectory;
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
final class OnLinkParsedEvent extends EventArgs
{
public const ON_LINK_PARSED = 'onLinkParsed';
/** @var string */
private $url;
/** @var string */
private $linkType;
/** @var string */
private $currentFileName;
public function __construct(string $url, string $linkType, string $currentFileName)
{
$this->url = $url;
$this->linkType = $linkType;
$this->currentFileName = $currentFileName;
}
public function getUrl(): string
{
return $this->url;
}
public function getLinkType(): string
{
return $this->linkType;
}
public function getCurrentFileName(): string
{
return $this->currentFileName;
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
final class PostBuildRenderEvent extends BuildEvent
{
public const POST_BUILD_RENDER = 'postBuildRender';
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
use Doctrine\RST\Nodes\Node;
final class PostNodeCreateEvent extends EventArgs
{
public const POST_NODE_CREATE = 'postNodeCreate';
/** @var Node */
private $node;
public function __construct(Node $node)
{
$this->node = $node;
}
public function getNode(): Node
{
return $this->node;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
use Doctrine\RST\Renderers\RenderedNode;
final class PostNodeRenderEvent extends EventArgs
{
public const POST_NODE_RENDER = 'postNodeRender';
/** @var RenderedNode */
private $renderedNode;
public function __construct(RenderedNode $renderedNode)
{
$this->renderedNode = $renderedNode;
}
public function getRenderedNode(): RenderedNode
{
return $this->renderedNode;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
use Doctrine\RST\Nodes\DocumentNode;
final class PostParseDocumentEvent extends EventArgs
{
public const POST_PARSE_DOCUMENT = 'postParseDocument';
/** @var DocumentNode */
private $documentNode;
public function __construct(DocumentNode $documentNode)
{
$this->documentNode = $documentNode;
}
public function getDocumentNode(): DocumentNode
{
return $this->documentNode;
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\RST\Builder;
final class PreBuildParseEvent extends BuildEvent
{
public const PRE_BUILD_PARSE = 'preBuildParse';
/** @var Builder\ParseQueue */
private $parseQueue;
public function __construct(Builder $builder, string $directory, string $targetDirectory, Builder\ParseQueue $parseQueue)
{
parent::__construct($builder, $directory, $targetDirectory);
$this->parseQueue = $parseQueue;
}
public function getParseQueue(): Builder\ParseQueue
{
return $this->parseQueue;
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
final class PreBuildRenderEvent extends BuildEvent
{
public const PRE_BUILD_RENDER = 'preBuildRender';
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
final class PreBuildScanEvent extends BuildEvent
{
public const PRE_BUILD_SCAN = 'preBuildScan';
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
use Doctrine\RST\Nodes\Node;
final class PreNodeRenderEvent extends EventArgs
{
public const PRE_NODE_RENDER = 'preNodeRender';
/** @var Node */
private $node;
public function __construct(Node $node)
{
$this->node = $node;
}
public function getNode(): Node
{
return $this->node;
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Event;
use Doctrine\Common\EventArgs;
use Doctrine\RST\Parser;
final class PreParseDocumentEvent extends EventArgs
{
public const PRE_PARSE_DOCUMENT = 'preParseDocument';
/** @var Parser */
private $parser;
/** @var string */
private $contents;
public function __construct(Parser $parser, string $contents)
{
$this->parser = $parser;
$this->contents = $contents;
}
public function getParser(): Parser
{
return $this->parser;
}
public function setContents(string $contents): void
{
$this->contents = $contents;
}
public function getContents(): string
{
return $this->contents;
}
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Exception;
use Exception;
final class InvalidTableStructure extends Exception
{
}

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use RuntimeException;
use function explode;
use function file_exists;
use function file_get_contents;
use function is_readable;
use function preg_replace_callback;
use function realpath;
use function sprintf;
use function strpos;
final class FileIncluder
{
/** @var Environment */
private $environment;
/** @var bool */
private $includeAllowed = true;
/** @var string */
private $includeRoot;
public function __construct(
Environment $environment,
bool $includeAllowed,
string $includeRoot
) {
$this->environment = $environment;
$this->includeAllowed = $includeAllowed;
$this->includeRoot = $includeRoot;
}
public function includeFiles(string $document): string
{
return (string) preg_replace_callback(
'/^\.\. include:: (.+)$/m',
function ($match): string {
$path = $this->environment->absoluteRelativePath($match[1]);
if (! file_exists($path)) {
throw new RuntimeException(sprintf('Include "%s" does not exist or is not readable.', $match[0]));
}
if ($this->isFileIncludeAllowed($path)) {
$contents = file_get_contents($path);
if ($contents === false) {
throw new RuntimeException(sprintf('Could not load file from path %s', $path));
}
return $this->includeFiles($contents);
}
return '';
},
$document
);
}
private function isFileIncludeAllowed(string $path): bool
{
if (! $this->includeAllowed) {
return false;
}
if (! @is_readable($path)) {
return false;
}
if ($this->includeRoot === '') {
return true;
}
$real = realpath($path);
if ($real === false) {
return false;
}
foreach (explode(':', $this->includeRoot) as $root) {
if (strpos($real, $root) === 0) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Formats;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Renderers\NodeRendererFactory;
interface Format
{
public const HTML = 'html';
public const LATEX = 'tex';
public function getFileExtension(): string;
/** @return Directive[] */
public function getDirectives(): array;
/** @return NodeRendererFactory[] */
public function getNodeRendererFactories(): array;
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Formats;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Renderers\NodeRendererFactory;
final class InternalFormat implements Format
{
/** @var Format */
private $format;
/** @var Directive[]|null */
private $directives;
/** @var NodeRendererFactory[]|null */
private $nodeRendererFactories;
public function __construct(Format $format)
{
$this->format = $format;
}
public function getFileExtension(): string
{
return $this->format->getFileExtension();
}
/** @return Directive[] */
public function getDirectives(): array
{
if ($this->directives === null) {
$this->directives = $this->format->getDirectives();
}
return $this->directives;
}
/** @return NodeRendererFactory[] */
public function getNodeRendererFactories(): array
{
if ($this->nodeRendererFactories === null) {
$this->nodeRendererFactories = $this->format->getNodeRendererFactories();
}
return $this->nodeRendererFactories;
}
}

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\SubDirective;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use function array_map;
use function explode;
final class ClassDirective extends SubDirective
{
public function getName(): string
{
return 'class';
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
if ($document === null) {
return null;
}
$classes = explode(' ', $data);
$normalizedClasses = array_map(static function (string $class): string {
return Environment::slugify($class);
}, $classes);
$document->setClasses($normalizedClasses);
if ($document instanceof DocumentNode) {
$this->setNodesClasses($document->getNodes(), $classes);
}
return $document;
}
public function appliesToNonBlockContent(): bool
{
return true;
}
/**
* @param Node[] $nodes
* @param string[] $classes
*/
private function setNodesClasses(array $nodes, array $classes): void
{
foreach ($nodes as $node) {
$node->setClasses($classes);
if (! ($node instanceof DocumentNode)) {
continue;
}
$this->setNodesClasses($node->getNodes(), $classes);
}
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\SubDirective;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Divs a sub document in a div with a given class
*/
final class Div extends SubDirective
{
public function getName(): string
{
return 'div';
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
$divOpen = $parser->renderTemplate('div-open.html.twig', ['class' => $data]);
return $parser->getNodeFactory()->createWrapperNode($document, $divOpen, '</div>');
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\SubDirective;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use Exception;
use function sprintf;
/**
* Renders an image, example :
*
* .. figure:: image.jpg
* :width: 100
* :title: An image
*
* Here is an awesome caption
*/
final class Figure extends SubDirective
{
public function getName(): string
{
return 'figure';
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
$environment = $parser->getEnvironment();
$url = $environment->relativeUrl($data);
if ($url === null) {
throw new Exception(sprintf('Could not get relative url for %s', $data));
}
$nodeFactory = $parser->getNodeFactory();
return $parser->getNodeFactory()->createFigureNode(
$nodeFactory->createImageNode($url, $options),
$document
);
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use Exception;
use function sprintf;
/**
* Renders an image, example :
*
* .. image:: image.jpg
* :width: 100
* :title: An image
*/
final class Image extends Directive
{
public function getName(): string
{
return 'image';
}
/** @param string[] $options */
public function processNode(
Parser $parser,
string $variable,
string $data,
array $options
): ?Node {
$environment = $parser->getEnvironment();
$url = $environment->relativeUrl($data);
if ($url === null) {
throw new Exception(sprintf('Could not get relative url for %s', $data));
}
return $parser->getNodeFactory()->createImageNode($url, $options);
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Add a meta information:
*
* .. meta::
* :key: value
*/
final class Meta extends Directive
{
public function getName(): string
{
return 'meta';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
$document = $parser->getDocument();
$nodeFactory = $parser->getNodeFactory();
foreach ($options as $key => $value) {
$meta = $nodeFactory->createMetaNode($key, $value);
$document->addHeaderNode($meta);
}
if ($node === null) {
return;
}
$document->addNode($node);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Adds a stylesheet to a document, example:
*
* .. stylesheet:: style.css
*/
final class Stylesheet extends Directive
{
public function getName(): string
{
return 'stylesheet';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
$document = $parser->getDocument();
$document->addCss($data);
if ($node === null) {
return;
}
$document->addNode($node);
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Add a meta title to the document
*
* .. title:: Page title
*/
final class Title extends Directive
{
public function getName(): string
{
return 'title';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
$document = $parser->getDocument();
$title = $parser->renderTemplate('title.html.twig', ['title' => $data]);
$document->addHeaderNode(
$parser->getNodeFactory()->createRawNode($title)
);
if ($node === null) {
return;
}
$document->addNode($node);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Parser;
use function trim;
/**
* Sets the document URL
*/
final class Url extends Directive
{
public function getName(): string
{
return 'url';
}
/** @param string[] $options */
public function processAction(
Parser $parser,
string $variable,
string $data,
array $options
): void {
$environment = $parser->getEnvironment();
$environment->setUrl(trim($data));
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Directives;
use Doctrine\RST\Directives\SubDirective;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use RuntimeException;
use function uniqid;
/**
* Wraps a sub document in a div with a given class
*/
final class Wrap extends SubDirective
{
/** @var string */
private $class;
/** @var bool */
private $uniqid;
public function __construct(string $class, bool $uniqid = false)
{
$this->class = $class;
$this->uniqid = $uniqid;
}
public function getName(): string
{
return $this->class;
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
// if there is a "Wrap" directive (e.g. a note::), blank content is unexpected
if ($document === null) {
throw new RuntimeException('Content expected, none found.');
}
if ($this->uniqid) {
$id = uniqid($this->class);
} else {
$id = '';
}
$divOpen = $parser->renderTemplate('div-open.html.twig', [
'id' => $id,
'class' => $this->class,
]);
return $parser->getNodeFactory()->createWrapperNode(
$document,
$divOpen,
'</div>'
);
}
}

View file

@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Formats\Format;
use Doctrine\RST\HTML;
use Doctrine\RST\Nodes;
use Doctrine\RST\Renderers;
use Doctrine\RST\Renderers\CallableNodeRendererFactory;
use Doctrine\RST\Renderers\NodeRendererFactory;
use Doctrine\RST\Templates\TemplateRenderer;
final class HTMLFormat implements Format
{
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TemplateRenderer $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function getFileExtension(): string
{
return Format::HTML;
}
/** @return Directive[] */
public function getDirectives(): array
{
return [
new HTML\Directives\Image(),
new HTML\Directives\Figure(),
new HTML\Directives\Meta(),
new HTML\Directives\Stylesheet(),
new HTML\Directives\Title(),
new HTML\Directives\Url(),
new HTML\Directives\Div(),
new HTML\Directives\Wrap('note'),
new HTML\Directives\ClassDirective(),
];
}
/** @return NodeRendererFactory[] */
public function getNodeRendererFactories(): array
{
return [
Nodes\AnchorNode::class => new CallableNodeRendererFactory(
function (Nodes\AnchorNode $node): HTML\Renderers\AnchorNodeRenderer {
return new HTML\Renderers\AnchorNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\CodeNode::class => new CallableNodeRendererFactory(
function (Nodes\CodeNode $node): HTML\Renderers\CodeNodeRenderer {
return new HTML\Renderers\CodeNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\DefinitionListNode::class => new CallableNodeRendererFactory(
function (Nodes\DefinitionListNode $node): HTML\Renderers\DefinitionListNodeRenderer {
return new HTML\Renderers\DefinitionListNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\FigureNode::class => new CallableNodeRendererFactory(
function (Nodes\FigureNode $node): HTML\Renderers\FigureNodeRenderer {
return new HTML\Renderers\FigureNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\ImageNode::class => new CallableNodeRendererFactory(
function (Nodes\ImageNode $node): HTML\Renderers\ImageNodeRenderer {
return new HTML\Renderers\ImageNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\ListNode::class => new CallableNodeRendererFactory(
function (Nodes\ListNode $node): HTML\Renderers\ListNodeRenderer {
return new HTML\Renderers\ListNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\MetaNode::class => new CallableNodeRendererFactory(
function (Nodes\MetaNode $node): HTML\Renderers\MetaNodeRenderer {
return new HTML\Renderers\MetaNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\ParagraphNode::class => new CallableNodeRendererFactory(
function (Nodes\ParagraphNode $node): HTML\Renderers\ParagraphNodeRenderer {
return new HTML\Renderers\ParagraphNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\QuoteNode::class => new CallableNodeRendererFactory(
function (Nodes\QuoteNode $node): HTML\Renderers\QuoteNodeRenderer {
return new HTML\Renderers\QuoteNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\SeparatorNode::class => new CallableNodeRendererFactory(
function (Nodes\SeparatorNode $node): HTML\Renderers\SeparatorNodeRenderer {
return new HTML\Renderers\SeparatorNodeRenderer(
$this->templateRenderer
);
}
),
Nodes\TableNode::class => new CallableNodeRendererFactory(
function (Nodes\TableNode $node): HTML\Renderers\TableNodeRenderer {
return new HTML\Renderers\TableNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\TitleNode::class => new CallableNodeRendererFactory(
function (Nodes\TitleNode $node): HTML\Renderers\TitleNodeRenderer {
return new HTML\Renderers\TitleNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\TocNode::class => new CallableNodeRendererFactory(
function (Nodes\TocNode $node): HTML\Renderers\TocNodeRenderer {
return new HTML\Renderers\TocNodeRenderer(
$node->getEnvironment(),
$node,
$this->templateRenderer
);
}
),
Nodes\DocumentNode::class => new CallableNodeRendererFactory(
function (Nodes\DocumentNode $node): HTML\Renderers\DocumentNodeRenderer {
return new HTML\Renderers\DocumentNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\SpanNode::class => new CallableNodeRendererFactory(
function (Nodes\SpanNode $node): HTML\Renderers\SpanNodeRenderer {
return new HTML\Renderers\SpanNodeRenderer(
$node->getEnvironment(),
$node,
$this->templateRenderer
);
}
),
Nodes\CallableNode::class => new CallableNodeRendererFactory(
static function (Nodes\CallableNode $node): Renderers\CallableNodeRenderer {
return new Renderers\CallableNodeRenderer(
$node
);
}
),
Nodes\SectionBeginNode::class => new CallableNodeRendererFactory(
function (Nodes\SectionBeginNode $node): HTML\Renderers\SectionBeginNodeRenderer {
return new HTML\Renderers\SectionBeginNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\SectionEndNode::class => new CallableNodeRendererFactory(
function (Nodes\SectionEndNode $node): HTML\Renderers\SectionEndNodeRenderer {
return new HTML\Renderers\SectionEndNodeRenderer(
$node,
$this->templateRenderer
);
}
),
];
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\AnchorNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class AnchorNodeRenderer implements NodeRenderer
{
/** @var AnchorNode */
private $anchorNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(AnchorNode $anchorNode, TemplateRenderer $templateRenderer)
{
$this->anchorNode = $anchorNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('anchor.html.twig', [
'anchorNode' => $this->anchorNode,
]);
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class CodeNodeRenderer implements NodeRenderer
{
/** @var CodeNode */
private $codeNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(CodeNode $codeNode, TemplateRenderer $templateRenderer)
{
$this->codeNode = $codeNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
if ($this->codeNode->isRaw()) {
return $this->codeNode->getValue();
}
return $this->templateRenderer->render('code.html.twig', [
'codeNode' => $this->codeNode,
]);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\DefinitionListNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class DefinitionListNodeRenderer implements NodeRenderer
{
/** @var DefinitionListNode */
private $definitionListNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(DefinitionListNode $definitionListNode, TemplateRenderer $templateRenderer)
{
$this->definitionListNode = $definitionListNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('definition-list.html.twig', [
'definitionListNode' => $this->definitionListNode,
'definitionList' => $this->definitionListNode->getDefinitionList(),
]);
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Renderers\DocumentNodeRenderer as BaseDocumentRender;
use Doctrine\RST\Renderers\FullDocumentNodeRenderer;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
use Gajus\Dindent\Indenter;
final class DocumentNodeRenderer implements NodeRenderer, FullDocumentNodeRenderer
{
/** @var DocumentNode */
private $document;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(DocumentNode $document, TemplateRenderer $templateRenderer)
{
$this->document = $document;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return (new BaseDocumentRender($this->document))->render();
}
public function renderDocument(): string
{
$headerNodes = '';
foreach ($this->document->getHeaderNodes() as $node) {
$headerNodes .= $node->render() . "\n";
}
$html = $this->templateRenderer->render('document.html.twig', [
'headerNodes' => $headerNodes,
'bodyNodes' => $this->render(),
]);
if ($this->document->getConfiguration()->getIndentHTML()) {
return $this->indentHTML($html);
}
return $html;
}
private function indentHTML(string $html): string
{
return (new Indenter())->indent($html);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\FigureNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class FigureNodeRenderer implements NodeRenderer
{
/** @var FigureNode */
private $figureNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(FigureNode $figureNode, TemplateRenderer $templateRenderer)
{
$this->figureNode = $figureNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('figure.html.twig', [
'figureNode' => $this->figureNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\ImageNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class ImageNodeRenderer implements NodeRenderer
{
/** @var ImageNode */
private $imageNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(ImageNode $imageNode, TemplateRenderer $templateRenderer)
{
$this->imageNode = $imageNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('image.html.twig', [
'imageNode' => $this->imageNode,
]);
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\ListNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class ListNodeRenderer implements NodeRenderer
{
/** @var ListNode */
private $listNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(ListNode $listNode, TemplateRenderer $templateRenderer)
{
$this->listNode = $listNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
$template = 'bullet-list.html.twig';
if ($this->listNode->isOrdered()) {
$template = 'enumerated-list.html.twig';
}
return $this->templateRenderer->render($template, ['listNode' => $this->listNode]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\MetaNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class MetaNodeRenderer implements NodeRenderer
{
/** @var MetaNode */
private $metaNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(MetaNode $metaNode, TemplateRenderer $templateRenderer)
{
$this->metaNode = $metaNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('meta.html.twig', [
'metaNode' => $this->metaNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\ParagraphNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class ParagraphNodeRenderer implements NodeRenderer
{
/** @var ParagraphNode */
private $paragraphNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(ParagraphNode $paragraphNode, TemplateRenderer $templateRenderer)
{
$this->paragraphNode = $paragraphNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('paragraph.html.twig', [
'paragraphNode' => $this->paragraphNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\QuoteNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class QuoteNodeRenderer implements NodeRenderer
{
/** @var QuoteNode */
private $quoteNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(QuoteNode $quoteNode, TemplateRenderer $templateRenderer)
{
$this->quoteNode = $quoteNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('quote.html.twig', [
'quoteNode' => $this->quoteNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\SectionBeginNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class SectionBeginNodeRenderer implements NodeRenderer
{
/** @var SectionBeginNode */
private $sectionBeginNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(SectionBeginNode $sectionBeginNode, TemplateRenderer $templateRenderer)
{
$this->sectionBeginNode = $sectionBeginNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('section-begin.html.twig', [
'sectionBeginNode' => $this->sectionBeginNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\SectionEndNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class SectionEndNodeRenderer implements NodeRenderer
{
/** @var SectionEndNode */
private $sectionEndNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(SectionEndNode $sectionEndNode, TemplateRenderer $templateRenderer)
{
$this->sectionEndNode = $sectionEndNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('section-end.html.twig', [
'sectionEndNode' => $this->sectionEndNode,
]);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class SeparatorNodeRenderer implements NodeRenderer
{
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TemplateRenderer $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('separator.html.twig');
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\SpanNode;
use Doctrine\RST\References\ResolvedReference;
use Doctrine\RST\Renderers\SpanNodeRenderer as BaseSpanNodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
use function htmlspecialchars;
use function trim;
use const ENT_COMPAT;
final class SpanNodeRenderer extends BaseSpanNodeRenderer
{
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(
Environment $environment,
SpanNode $span,
TemplateRenderer $templateRenderer
) {
parent::__construct($environment, $span);
$this->templateRenderer = $templateRenderer;
}
public function emphasis(string $text): string
{
return $this->templateRenderer->render('emphasis.html.twig', ['text' => $text]);
}
public function strongEmphasis(string $text): string
{
return $this->templateRenderer->render('strong-emphasis.html.twig', ['text' => $text]);
}
public function nbsp(): string
{
return $this->templateRenderer->render('nbsp.html.twig');
}
public function br(): string
{
return $this->templateRenderer->render('br.html.twig');
}
public function literal(string $text): string
{
return $this->templateRenderer->render('literal.html.twig', ['text' => $text]);
}
/** @param mixed[] $attributes */
public function link(?string $url, string $title, array $attributes = []): string
{
$url = (string) $url;
return $this->templateRenderer->render('link.html.twig', [
'url' => $this->environment->generateUrl($url),
'title' => $title,
'attributes' => $attributes,
]);
}
public function escape(string $span): string
{
return htmlspecialchars($span, ENT_COMPAT);
}
/** @param mixed[] $value */
public function reference(ResolvedReference $reference, array $value): string
{
$text = (bool) $value['text'] ? $value['text'] : ($reference->getTitle() ?? '');
$text = trim($text);
// reference to another document
if ($reference->getUrl() !== null) {
$url = $reference->getUrl();
if ($value['anchor'] !== null) {
$url .= '#' . $value['anchor'];
}
$link = $this->link($url, $text, $reference->getAttributes());
// reference to anchor in existing document
} elseif ($value['url'] !== null) {
$url = $this->environment->getLink($value['url']);
$link = $this->link($url, $text, $reference->getAttributes());
} else {
$link = $this->link('#', $text . ' (unresolved reference)', $reference->getAttributes());
}
return $link;
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\TableNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
use LogicException;
use function sprintf;
final class TableNodeRenderer implements NodeRenderer
{
/** @var TableNode */
private $tableNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TableNode $tableNode, TemplateRenderer $templateRenderer)
{
$this->tableNode = $tableNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
$headers = $this->tableNode->getHeaders();
$rows = $this->tableNode->getData();
$tableHeaderRows = [];
foreach ($headers as $k => $isHeader) {
if ($isHeader === false) {
continue;
}
if (! isset($rows[$k])) {
throw new LogicException(sprintf('Row "%d" should be a header, but that row does not exist.', $k));
}
$tableHeaderRows[] = $rows[$k];
unset($rows[$k]);
}
return $this->templateRenderer->render('table.html.twig', [
'tableNode' => $this->tableNode,
'tableHeaderRows' => $tableHeaderRows,
'tableRows' => $rows,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Nodes\TitleNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class TitleNodeRenderer implements NodeRenderer
{
/** @var TitleNode */
private $titleNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TitleNode $titleNode, TemplateRenderer $templateRenderer)
{
$this->titleNode = $titleNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('header-title.html.twig', [
'titleNode' => $this->titleNode,
]);
}
}

View file

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\HTML\Renderers;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\TocNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
use function count;
use function is_array;
final class TocNodeRenderer implements NodeRenderer
{
/** @var Environment */
private $environment;
/** @var TocNode */
private $tocNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(Environment $environment, TocNode $tocNode, TemplateRenderer $templateRenderer)
{
$this->environment = $environment;
$this->tocNode = $tocNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
$options = $this->tocNode->getOptions();
if (isset($options['hidden'])) {
return '';
}
$tocItems = [];
foreach ($this->tocNode->getFiles() as $file) {
$reference = $this->environment->resolve('doc', $file);
if ($reference === null) {
continue;
}
$url = $this->environment->relativeUrl($reference->getUrl());
$this->buildLevel($url, $reference->getTitles(), 1, $tocItems, $file);
}
return $this->templateRenderer->render('toc.html.twig', [
'tocNode' => $this->tocNode,
'tocItems' => $tocItems,
]);
}
/**
* @param mixed[]|array $titles
* @param mixed[] $tocItems
*/
private function buildLevel(
?string $url,
array $titles,
int $level,
array &$tocItems,
string $file
): void {
foreach ($titles as $k => $entry) {
[$title, $children] = $entry;
[$title, $target] = $this->generateTarget(
$url,
$title,
// don't add anchor for first h1 in a different file (link directly to the file)
! ($level === 1 && $k === 0 && $file !== '/' . $this->environment->getCurrentFileName())
);
$tocItem = [
'targetId' => $this->generateTargetId($target),
'targetUrl' => $this->environment->generateUrl($target),
'title' => $title,
'level' => $level,
'children' => [],
];
// render children until we hit the configured maxdepth
if (count($children) > 0 && ! $this->tocNode->isTitlesOnly() && $level < $this->tocNode->getDepth()) {
$this->buildLevel($url, $children, $level + 1, $tocItem['children'], $file);
}
$tocItems[] = $tocItem;
}
}
private function generateTargetId(string $target): string
{
return Environment::slugify($target);
}
/**
* @param string[]|string $title
*
* @return mixed[]
*/
private function generateTarget(?string $url, $title, bool $withAnchor): array
{
$target = $url;
if ($withAnchor) {
$anchor = $this->generateAnchorFromTitle($title);
$target .= '#' . $anchor;
}
if (is_array($title)) {
[$title, $target] = $title;
$reference = $this->environment->resolve('doc', $target);
if ($reference === null) {
return [$title, $target];
}
$target = $this->environment->relativeUrl($reference->getUrl());
}
return [$title, $target];
}
/** @param string[]|string $title */
private function generateAnchorFromTitle($title): string
{
$slug = is_array($title)
? $title[1]
: $title;
return Environment::slugify($slug);
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
final class InvalidLink
{
/** @var string */
private $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\References\Doc;
use Doctrine\RST\References\Reference;
use function array_merge;
class Kernel
{
/** @var Configuration */
private $configuration;
/** @var Directive[] */
private $directives;
/** @var Reference[] */
private $references;
/**
* @param Directive[] $directives
* @param Reference[] $references
*/
public function __construct(
?Configuration $configuration = null,
array $directives = [],
array $references = []
) {
$this->configuration = $configuration ?? new Configuration();
$this->directives = array_merge([
new Directives\Dummy(),
new Directives\CodeBlock(),
new Directives\Raw(),
new Directives\Replace(),
new Directives\Tip(),
new Directives\Toctree(),
], $this->configuration->getFormat()->getDirectives(), $directives);
$this->references = array_merge([
new References\Doc(),
new References\Doc('ref', true),
], $this->createReferences(), $references);
}
public function getConfiguration(): Configuration
{
return $this->configuration;
}
/** @return Directive[] */
public function getDirectives(): array
{
return $this->directives;
}
/** @return Reference[] */
public function getReferences(): array
{
return $this->references;
}
public function postParse(DocumentNode $document): void
{
}
public function initBuilder(Builder $builder): void
{
}
/** @return Doc[] */
protected function createReferences(): array
{
return [];
}
}

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
use Exception;
use function sprintf;
/**
* Renders an image, example :
*
* .. image:: image.jpg
* :width: 100
* :title: An image
*/
final class Image extends Directive
{
public function getName(): string
{
return 'image';
}
/** @param string[] $options */
public function processNode(
Parser $parser,
string $variable,
string $data,
array $options
): ?Node {
$environment = $parser->getEnvironment();
$url = $environment->relativeUrl($data);
if ($url === null) {
throw new Exception(sprintf('Could not build relative url for %s', $data));
}
return $parser->getNodeFactory()->createImageNode($url, $options);
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Marks the document as LaTeX main
*/
final class LaTeXMain extends Directive
{
public function getName(): string
{
return 'latex-main';
}
/** @param string[] $options */
public function processNode(
Parser $parser,
string $variable,
string $data,
array $options
): ?Node {
return $parser->getNodeFactory()->createMainNode();
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Add a meta information:
*
* .. meta::
* :key: value
*/
final class Meta extends Directive
{
public function getName(): string
{
return 'meta';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
$document = $parser->getDocument();
$nodeFactory = $parser->getNodeFactory();
foreach ($options as $key => $value) {
$meta = $nodeFactory->createMetaNode($key, $value);
$document->addHeaderNode($meta);
}
if ($node === null) {
return;
}
$document->addNode($node);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Adds a stylesheet to a document, example:
*
* .. stylesheet:: style.css
*/
final class Stylesheet extends Directive
{
public function getName(): string
{
return 'stylesheet';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Add a meta title to the document
*
* .. title:: Page title
*/
final class Title extends Directive
{
public function getName(): string
{
return 'title';
}
/** @param string[] $options */
public function process(
Parser $parser,
?Node $node,
string $variable,
string $data,
array $options
): void {
$document = $parser->getDocument();
$document->addHeaderNode(
$parser->getNodeFactory()->createRawNode('\title{' . $data . '}')
);
if ($node === null) {
return;
}
$document->addNode($node);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Parser;
use function trim;
/**
* Sets the document URL
*/
final class Url extends Directive
{
public function getName(): string
{
return 'url';
}
/** @param string[] $options */
public function processAction(
Parser $parser,
string $variable,
string $data,
array $options
): void {
$environment = $parser->getEnvironment();
$environment->setUrl(trim($data));
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Directives;
use Doctrine\RST\Directives\SubDirective;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Parser;
/**
* Wraps a sub document in a div with a given class
*/
final class Wrap extends SubDirective
{
/** @var string */
private $class;
public function __construct(string $class)
{
$this->class = $class;
}
public function getName(): string
{
return $this->class;
}
/** @param string[] $options */
public function processSub(
Parser $parser,
?Node $document,
string $variable,
string $data,
array $options
): ?Node {
return $document;
}
}

View file

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX;
use Doctrine\RST\Directives\Directive;
use Doctrine\RST\Formats\Format;
use Doctrine\RST\LaTeX;
use Doctrine\RST\Nodes;
use Doctrine\RST\Renderers;
use Doctrine\RST\Renderers\CallableNodeRendererFactory;
use Doctrine\RST\Renderers\NodeRendererFactory;
use Doctrine\RST\Templates\TemplateRenderer;
final class LaTeXFormat implements Format
{
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TemplateRenderer $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function getFileExtension(): string
{
return Format::LATEX;
}
/** @return Directive[] */
public function getDirectives(): array
{
return [
new LaTeX\Directives\LaTeXMain(),
new LaTeX\Directives\Image(),
new LaTeX\Directives\Meta(),
new LaTeX\Directives\Stylesheet(),
new LaTeX\Directives\Title(),
new LaTeX\Directives\Url(),
new LaTeX\Directives\Wrap('note'),
];
}
/** @return NodeRendererFactory[] */
public function getNodeRendererFactories(): array
{
return [
Nodes\AnchorNode::class => new CallableNodeRendererFactory(
function (Nodes\AnchorNode $node): LaTeX\Renderers\AnchorNodeRenderer {
return new LaTeX\Renderers\AnchorNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\CodeNode::class => new CallableNodeRendererFactory(
function (Nodes\CodeNode $node): LaTeX\Renderers\CodeNodeRenderer {
return new LaTeX\Renderers\CodeNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\ImageNode::class => new CallableNodeRendererFactory(
function (Nodes\ImageNode $node): LaTeX\Renderers\ImageNodeRenderer {
return new LaTeX\Renderers\ImageNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\ListNode::class => new CallableNodeRendererFactory(
function (Nodes\ListNode $node): LaTeX\Renderers\ListNodeRenderer {
return new LaTeX\Renderers\ListNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\MetaNode::class => new CallableNodeRendererFactory(
function (Nodes\MetaNode $node): LaTeX\Renderers\MetaNodeRenderer {
return new LaTeX\Renderers\MetaNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\ParagraphNode::class => new CallableNodeRendererFactory(
function (Nodes\ParagraphNode $node): LaTeX\Renderers\ParagraphNodeRenderer {
return new LaTeX\Renderers\ParagraphNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\QuoteNode::class => new CallableNodeRendererFactory(
function (Nodes\QuoteNode $node): LaTeX\Renderers\QuoteNodeRenderer {
return new LaTeX\Renderers\QuoteNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\SeparatorNode::class => new CallableNodeRendererFactory(
function (Nodes\SeparatorNode $node): LaTeX\Renderers\SeparatorNodeRenderer {
return new LaTeX\Renderers\SeparatorNodeRenderer(
$this->templateRenderer
);
}
),
Nodes\TableNode::class => new CallableNodeRendererFactory(
static function (Nodes\TableNode $node): LaTeX\Renderers\TableNodeRenderer {
return new LaTeX\Renderers\TableNodeRenderer(
$node
);
}
),
Nodes\TitleNode::class => new CallableNodeRendererFactory(
function (Nodes\TitleNode $node): LaTeX\Renderers\TitleNodeRenderer {
return new LaTeX\Renderers\TitleNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\TocNode::class => new CallableNodeRendererFactory(
function (Nodes\TocNode $node): LaTeX\Renderers\TocNodeRenderer {
return new LaTeX\Renderers\TocNodeRenderer(
$node->getEnvironment(),
$node,
$this->templateRenderer
);
}
),
Nodes\DocumentNode::class => new CallableNodeRendererFactory(
function (Nodes\DocumentNode $node): LaTeX\Renderers\DocumentNodeRenderer {
return new LaTeX\Renderers\DocumentNodeRenderer(
$node,
$this->templateRenderer
);
}
),
Nodes\SpanNode::class => new CallableNodeRendererFactory(
function (Nodes\SpanNode $node): LaTeX\Renderers\SpanNodeRenderer {
return new LaTeX\Renderers\SpanNodeRenderer(
$node->getEnvironment(),
$node,
$this->templateRenderer
);
}
),
Nodes\CallableNode::class => new CallableNodeRendererFactory(
static function (Nodes\CallableNode $node): Renderers\CallableNodeRenderer {
return new Renderers\CallableNodeRenderer(
$node
);
}
),
];
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\AnchorNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class AnchorNodeRenderer implements NodeRenderer
{
/** @var AnchorNode */
private $anchorNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(AnchorNode $anchorNode, TemplateRenderer $templateRenderer)
{
$this->anchorNode = $anchorNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('anchor.tex.twig', [
'anchorNode' => $this->anchorNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class CodeNodeRenderer implements NodeRenderer
{
/** @var CodeNode */
private $codeNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(CodeNode $codeNode, TemplateRenderer $templateRenderer)
{
$this->codeNode = $codeNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('code.tex.twig', [
'codeNode' => $this->codeNode,
]);
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Nodes\MainNode;
use Doctrine\RST\Renderers\DocumentNodeRenderer as BaseDocumentRender;
use Doctrine\RST\Renderers\FullDocumentNodeRenderer;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
use function count;
final class DocumentNodeRenderer implements NodeRenderer, FullDocumentNodeRenderer
{
/** @var DocumentNode */
private $document;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(DocumentNode $document, TemplateRenderer $templateRenderer)
{
$this->document = $document;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return (new BaseDocumentRender($this->document))->render();
}
public function renderDocument(): string
{
return $this->templateRenderer->render('document.tex.twig', [
'isMain' => $this->isMain(),
'document' => $this->document,
'body' => $this->render(),
]);
}
private function isMain(): bool
{
return count($this->document->getNodes(static function ($node): bool {
return $node instanceof MainNode;
})) !== 0;
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\ImageNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class ImageNodeRenderer implements NodeRenderer
{
/** @var ImageNode */
private $imageNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(ImageNode $imageNode, TemplateRenderer $templateRenderer)
{
$this->imageNode = $imageNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('image.tex.twig', [
'imageNode' => $this->imageNode,
]);
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Renderers\NodeRenderer;
final class LaTexMainNodeRenderer implements NodeRenderer
{
public function render(): string
{
return '';
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\ListNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class ListNodeRenderer implements NodeRenderer
{
/** @var ListNode */
private $listNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(ListNode $listNode, TemplateRenderer $templateRenderer)
{
$this->listNode = $listNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
$template = 'bullet-list.tex.twig';
if ($this->listNode->isOrdered()) {
$template = 'enumerated-list.tex.twig';
}
return $this->templateRenderer->render($template, [
'listNode' => $this->listNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\MetaNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class MetaNodeRenderer implements NodeRenderer
{
/** @var MetaNode */
private $metaNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(MetaNode $metaNode, TemplateRenderer $templateRenderer)
{
$this->metaNode = $metaNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('meta.tex.twig', [
'metaNode' => $this->metaNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\ParagraphNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class ParagraphNodeRenderer implements NodeRenderer
{
/** @var ParagraphNode */
private $paragraphNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(ParagraphNode $paragraphNode, TemplateRenderer $templateRenderer)
{
$this->paragraphNode = $paragraphNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('paragraph.tex.twig', [
'paragraphNode' => $this->paragraphNode,
]);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\QuoteNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class QuoteNodeRenderer implements NodeRenderer
{
/** @var QuoteNode */
private $quoteNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(QuoteNode $quoteNode, TemplateRenderer $templateRenderer)
{
$this->quoteNode = $quoteNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('quote.tex.twig', [
'quoteNode' => $this->quoteNode,
]);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class SeparatorNodeRenderer implements NodeRenderer
{
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TemplateRenderer $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
return $this->templateRenderer->render('separator.tex.twig');
}
}

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\SpanNode;
use Doctrine\RST\References\ResolvedReference;
use Doctrine\RST\Renderers\SpanNodeRenderer as BaseSpanNodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
use function is_string;
use function substr;
use function trim;
final class SpanNodeRenderer extends BaseSpanNodeRenderer
{
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(
Environment $environment,
SpanNode $span,
TemplateRenderer $templateRenderer
) {
parent::__construct($environment, $span);
$this->templateRenderer = $templateRenderer;
}
public function emphasis(string $text): string
{
return $this->templateRenderer->render('emphasis.tex.twig', ['text' => $text]);
}
public function strongEmphasis(string $text): string
{
return $this->templateRenderer->render('strong-emphasis.tex.twig', ['text' => $text]);
}
public function nbsp(): string
{
return $this->templateRenderer->render('nbsp.tex.twig');
}
public function br(): string
{
return $this->templateRenderer->render('br.tex.twig');
}
public function literal(string $text): string
{
return $this->templateRenderer->render('literal.tex.twig', ['text' => $text]);
}
/** @param mixed[] $attributes */
public function link(?string $url, string $title, array $attributes = []): string
{
$type = 'href';
if (is_string($url) && $url !== '' && $url[0] === '#') {
$type = 'ref';
$url = substr($url, 1);
$url = $url !== '' ? '#' . $url : '';
$url = $this->environment->getUrl() . $url;
}
return $this->templateRenderer->render('link.tex.twig', [
'type' => $type,
'url' => $url,
'title' => $title,
'attributes' => $attributes,
]);
}
public function escape(string $span): string
{
return $span;
}
/** @param mixed[] $value */
public function reference(ResolvedReference $reference, array $value): string
{
$text = (bool) $value['text'] ? $value['text'] : $reference->getTitle();
$url = $reference->getUrl();
if ($value['anchor'] !== '') {
$url .= $value['anchor'];
}
if ($text === null) {
$text = '';
}
if ($url === null) {
$url = '';
}
return $this->link($url, trim($text));
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\TableNode;
use Doctrine\RST\Renderers\NodeRenderer;
use function count;
use function implode;
use function max;
final class TableNodeRenderer implements NodeRenderer
{
/** @var TableNode */
private $tableNode;
public function __construct(TableNode $tableNode)
{
$this->tableNode = $tableNode;
}
public function render(): string
{
$cols = 0;
$rows = [];
foreach ($this->tableNode->getData() as $row) {
$rowTex = '';
$cols = max($cols, count($row->getColumns()));
foreach ($row->getColumns() as $n => $col) {
$rowTex .= $col->render();
if ((int) $n + 1 >= count($row->getColumns())) {
continue;
}
$rowTex .= ' & ';
}
$rowTex .= ' \\\\' . "\n";
$rows[] = $rowTex;
}
$aligns = [];
for ($i = 0; $i < $cols; $i++) {
$aligns[] = 'l';
}
$aligns = '|' . implode('|', $aligns) . '|';
$rows = "\\hline\n" . implode("\\hline\n", $rows) . "\\hline\n";
return "\\begin{tabular}{" . $aligns . "}\n" . $rows . "\n\\end{tabular}\n";
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Nodes\TitleNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class TitleNodeRenderer implements NodeRenderer
{
/** @var TitleNode */
private $titleNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(TitleNode $titleNode, TemplateRenderer $templateRenderer)
{
$this->titleNode = $titleNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
$type = 'chapter';
if ($this->titleNode->getLevel() > 1) {
$type = 'section';
for ($i = 2; $i < $this->titleNode->getLevel(); $i++) {
$type = 'sub' . $type;
}
}
return $this->templateRenderer->render('title.tex.twig', [
'type' => $type,
'titleNode' => $this->titleNode,
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\LaTeX\Renderers;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\TocNode;
use Doctrine\RST\Renderers\NodeRenderer;
use Doctrine\RST\Templates\TemplateRenderer;
final class TocNodeRenderer implements NodeRenderer
{
/** @var Environment */
private $environment;
/** @var TocNode */
private $tocNode;
/** @var TemplateRenderer */
private $templateRenderer;
public function __construct(Environment $environment, TocNode $tocNode, TemplateRenderer $templateRenderer)
{
$this->environment = $environment;
$this->tocNode = $tocNode;
$this->templateRenderer = $templateRenderer;
}
public function render(): string
{
$tocItems = [];
foreach ($this->tocNode->getFiles() as $file) {
$reference = $this->environment->resolve('doc', $file);
if ($reference === null) {
continue;
}
$url = $this->environment->relativeUrl($reference->getUrl());
$tocItems[] = ['url' => $url];
}
return $this->templateRenderer->render('toc.tex.twig', [
'tocNode' => $this->tocNode,
'tocItems' => $tocItems,
]);
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Meta;
use LogicException;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function serialize;
use function sprintf;
use function unserialize;
final class CachedMetasLoader
{
public function loadCachedMetaEntries(string $targetDirectory, Metas $metas): void
{
$metaCachePath = $this->getMetaCachePath($targetDirectory);
if (! file_exists($metaCachePath)) {
return;
}
$contents = file_get_contents($metaCachePath);
if ($contents === false) {
throw new LogicException(sprintf('Could not load file "%s"', $contents));
}
$metas->setMetaEntries(unserialize($contents));
}
public function cacheMetaEntries(string $targetDirectory, Metas $metas): void
{
file_put_contents($this->getMetaCachePath($targetDirectory), serialize($metas->getAll()));
}
private function getMetaCachePath(string $targetDirectory): string
{
return $targetDirectory . '/metas.php';
}
}

View file

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Meta;
use Doctrine\RST\Environment;
use LogicException;
use function array_merge;
use function array_search;
use function in_array;
use function is_array;
use function is_string;
use function sprintf;
class MetaEntry
{
/** @var string */
private $file;
/** @var string */
private $url;
/** @var string */
private $title;
/** @var string[][]|string[][][] */
private $titles;
/** @var mixed[][] */
private $tocs;
/** @var int */
private $mtime;
/** @var string[] */
private $depends;
/** @var string[] */
private $resolvedDependencies = [];
/** @var string[] */
private $links;
/** @var string|null */
private $parent;
/**
* @param string[][]|string[][][] $titles
* @param mixed[][] $tocs
* @param string[] $depends
* @param string[] $links
*/
public function __construct(
string $file,
string $url,
string $title,
array $titles,
array $tocs,
array $depends,
array $links,
int $mtime
) {
$this->file = $file;
$this->url = $url;
$this->title = $title;
$this->titles = $titles;
$this->tocs = $tocs;
$this->depends = $depends;
$this->links = $links;
$this->mtime = $mtime;
}
public function getFile(): string
{
return $this->file;
}
public function getUrl(): string
{
return $this->url;
}
public function getTitle(): string
{
return $this->title;
}
/** @return string[][]|string[][][] */
public function getTitles(): array
{
return $this->titles;
}
public function hasTitle(string $text): bool
{
$titles = $this->getAllTitles();
$text = Environment::slugify($text);
foreach ($titles as $title) {
if ($text === Environment::slugify($title)) {
return true;
}
}
return false;
}
/** @return mixed[][] */
public function getTocs(): array
{
return $this->tocs;
}
/** @return string[] */
public function getDepends(): array
{
return $this->depends;
}
/**
* Call to replace a dependency with the resolved, real filename.
*/
public function resolveDependency(string $originalDependency, ?string $newDependency): void
{
if ($newDependency === null) {
return;
}
// we only need to resolve a dependency one time
if (in_array($originalDependency, $this->resolvedDependencies, true)) {
return;
}
$key = array_search($originalDependency, $this->depends, true);
if ($key === false) {
throw new LogicException(sprintf('Could not find dependency "%s" in MetaEntry for "%s"', $originalDependency, $this->file));
}
$this->depends[$key] = $newDependency;
$this->resolvedDependencies[] = $originalDependency;
}
public function removeDependency(string $dependency): void
{
$key = array_search($dependency, $this->depends, true);
if ($key === false) {
return;
}
unset($this->depends[$key]);
}
/** @return string[] */
public function getLinks(): array
{
return $this->links;
}
public function getMtime(): int
{
return $this->mtime;
}
public function setParent(string $parent): void
{
$this->parent = $parent;
}
public function getParent(): ?string
{
return $this->parent;
}
/**
* @param string[]|string[][]|null $entryTitles
*
* @return string[]
*/
private function getAllTitles(?array $entryTitles = null): array
{
if ($entryTitles === null) {
$entryTitles = $this->titles;
}
$titles = [];
foreach ($entryTitles as $title) {
if (is_string($title[0])) {
$titles[] = $title[0];
}
if (! is_array($title[1])) {
continue;
}
$titles = array_merge($titles, $this->getAllTitles($title[1]));
}
return $titles;
}
}

View file

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Meta;
use Doctrine\RST\Environment;
use function strtolower;
class Metas
{
/** @var MetaEntry[] */
private $entries = [];
/** @var string[] */
private $parents = [];
/** @param MetaEntry[] $entries */
public function __construct(array $entries = [])
{
$this->entries = $entries;
}
public function findLinkMetaEntry(string $link): ?MetaEntry
{
foreach ($this->entries as $entry) {
if ($this->doesLinkExist($entry->getLinks(), $link)) {
return $entry;
}
}
return $this->findByTitle($link);
}
/** @return MetaEntry[] */
public function getAll(): array
{
return $this->entries;
}
/**
* @param string[][] $titles
* @param mixed[][] $tocs
* @param string[] $depends
* @param string[] $links
*/
public function set(
string $file,
string $url,
string $title,
array $titles,
array $tocs,
int $mtime,
array $depends,
array $links
): void {
foreach ($tocs as $toc) {
foreach ($toc as $child) {
$this->parents[$child] = $file;
if (! isset($this->entries[$child])) {
continue;
}
$this->entries[$child]->setParent($file);
}
}
$this->entries[$file] = new MetaEntry(
$file,
$url,
$title,
$titles,
$tocs,
$depends,
$links,
$mtime
);
if (! isset($this->parents[$file])) {
return;
}
$this->entries[$file]->setParent($this->parents[$file]);
}
public function get(string $url): ?MetaEntry
{
if (isset($this->entries[$url])) {
return $this->entries[$url];
}
return null;
}
/** @param MetaEntry[] $metaEntries */
public function setMetaEntries(array $metaEntries): void
{
$this->entries = $metaEntries;
}
/** @param string[] $links */
private function doesLinkExist(array $links, string $link): bool
{
foreach ($links as $name => $url) {
if ($name === strtolower($link)) {
return true;
}
}
return false;
}
private function findByTitle(string $text): ?MetaEntry
{
$text = Environment::slugify($text);
// try to lookup the document reference by title
foreach ($this->entries as $entry) {
if ($entry->hasTitle($text)) {
return $entry;
}
}
return null;
}
}

View file

@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\NodeFactory;
use Doctrine\Common\EventManager;
use Doctrine\RST\Environment;
use Doctrine\RST\Event\PostNodeCreateEvent;
use Doctrine\RST\Nodes\AnchorNode;
use Doctrine\RST\Nodes\BlockNode;
use Doctrine\RST\Nodes\CallableNode;
use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Nodes\DefinitionListNode;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Nodes\DummyNode;
use Doctrine\RST\Nodes\FigureNode;
use Doctrine\RST\Nodes\ImageNode;
use Doctrine\RST\Nodes\ListNode;
use Doctrine\RST\Nodes\MainNode;
use Doctrine\RST\Nodes\MetaNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Nodes\NodeTypes;
use Doctrine\RST\Nodes\ParagraphNode;
use Doctrine\RST\Nodes\QuoteNode;
use Doctrine\RST\Nodes\RawNode;
use Doctrine\RST\Nodes\SectionBeginNode;
use Doctrine\RST\Nodes\SectionEndNode;
use Doctrine\RST\Nodes\SeparatorNode;
use Doctrine\RST\Nodes\SpanNode;
use Doctrine\RST\Nodes\TableNode;
use Doctrine\RST\Nodes\TitleNode;
use Doctrine\RST\Nodes\TocNode;
use Doctrine\RST\Nodes\WrapperNode;
use Doctrine\RST\Parser;
use Doctrine\RST\Parser\DefinitionList;
use Doctrine\RST\Parser\LineChecker;
use Doctrine\RST\Parser\ListItem;
use InvalidArgumentException;
use function assert;
use function sprintf;
final class DefaultNodeFactory implements NodeFactory
{
/** @var EventManager */
private $eventManager;
/** @var NodeInstantiator[] */
private $nodeInstantiators = [];
public function __construct(EventManager $eventManager, NodeInstantiator ...$nodeInstantiators)
{
$this->eventManager = $eventManager;
foreach ($nodeInstantiators as $nodeInstantiator) {
$this->nodeInstantiators[$nodeInstantiator->getType()] = $nodeInstantiator;
}
}
public function createDocumentNode(Environment $environment): DocumentNode
{
$document = $this->create(NodeTypes::DOCUMENT, [$environment]);
assert($document instanceof DocumentNode);
return $document;
}
/**
* @param string[] $files
* @param string[] $options
*/
public function createTocNode(Environment $environment, array $files, array $options): TocNode
{
$tocNode = $this->create(NodeTypes::TOC, [$environment, $files, $options]);
assert($tocNode instanceof TocNode);
return $tocNode;
}
public function createTitleNode(Node $value, int $level, string $token): TitleNode
{
$titleNode = $this->create(NodeTypes::TITLE, [$value, $level, $token]);
assert($titleNode instanceof TitleNode);
return $titleNode;
}
public function createSeparatorNode(int $level): SeparatorNode
{
$separatorNode = $this->create(NodeTypes::SEPARATOR, [$level]);
assert($separatorNode instanceof SeparatorNode);
return $separatorNode;
}
/** @param string[] $lines */
public function createBlockNode(array $lines): BlockNode
{
$blockNode = $this->create(NodeTypes::BLOCK, [$lines]);
assert($blockNode instanceof BlockNode);
return $blockNode;
}
/** @param string[] $lines */
public function createCodeNode(array $lines): CodeNode
{
$codeNode = $this->create(NodeTypes::CODE, [$lines]);
assert($codeNode instanceof CodeNode);
return $codeNode;
}
public function createQuoteNode(DocumentNode $documentNode): QuoteNode
{
$quoteNode = $this->create(NodeTypes::QUOTE, [$documentNode]);
assert($quoteNode instanceof QuoteNode);
return $quoteNode;
}
public function createParagraphNode(SpanNode $span): ParagraphNode
{
$paragraphNode = $this->create(NodeTypes::PARAGRAPH, [$span]);
assert($paragraphNode instanceof ParagraphNode);
return $paragraphNode;
}
public function createAnchorNode(?string $value = null): AnchorNode
{
$anchorNode = $this->create(NodeTypes::ANCHOR, [$value]);
assert($anchorNode instanceof AnchorNode);
return $anchorNode;
}
/** @param ListItem[] $items */
public function createListNode(array $items, bool $ordered): ListNode
{
$listNode = $this->create(NodeTypes::LIST, [$items, $ordered]);
assert($listNode instanceof ListNode);
return $listNode;
}
public function createTableNode(Parser\TableSeparatorLineConfig $separatorLineConfig, string $type, LineChecker $lineChecker): TableNode
{
$tableNode = $this->create(NodeTypes::TABLE, [$separatorLineConfig, $type, $lineChecker]);
assert($tableNode instanceof TableNode);
return $tableNode;
}
/** @param string|string[]|SpanNode $span */
public function createSpanNode(Parser $parser, $span): SpanNode
{
$span = $this->create(NodeTypes::SPAN, [$parser, $span]);
assert($span instanceof SpanNode);
return $span;
}
public function createDefinitionListNode(DefinitionList $definitionList): DefinitionListNode
{
$definitionListNode = $this->create(NodeTypes::DEFINITION_LIST, [$definitionList]);
assert($definitionListNode instanceof DefinitionListNode);
return $definitionListNode;
}
public function createWrapperNode(?Node $node, string $before = '', string $after = ''): WrapperNode
{
$wrapperNode = $this->create(NodeTypes::WRAPPER, [$node, $before, $after]);
assert($wrapperNode instanceof WrapperNode);
return $wrapperNode;
}
public function createFigureNode(ImageNode $image, ?Node $document = null): FigureNode
{
$figureNode = $this->create(NodeTypes::FIGURE, [$image, $document]);
assert($figureNode instanceof FigureNode);
return $figureNode;
}
/** @param string[] $options */
public function createImageNode(string $url, array $options = []): ImageNode
{
$imageNode = $this->create(NodeTypes::IMAGE, [$url, $options]);
assert($imageNode instanceof ImageNode);
return $imageNode;
}
public function createMetaNode(string $key, string $value): MetaNode
{
$metaNode = $this->create(NodeTypes::META, [$key, $value]);
assert($metaNode instanceof MetaNode);
return $metaNode;
}
public function createRawNode(string $value): RawNode
{
$rawNode = $this->create(NodeTypes::RAW, [$value]);
assert($rawNode instanceof RawNode);
return $rawNode;
}
/** @param mixed[] $data */
public function createDummyNode(array $data): DummyNode
{
$dummyNode = $this->create(NodeTypes::DUMMY, [$data]);
assert($dummyNode instanceof DummyNode);
return $dummyNode;
}
public function createMainNode(): MainNode
{
$mainNode = $this->create(NodeTypes::MAIN, []);
assert($mainNode instanceof MainNode);
return $mainNode;
}
public function createCallableNode(callable $callable): CallableNode
{
$callableNode = $this->create(NodeTypes::CALLABLE, [$callable]);
assert($callableNode instanceof CallableNode);
return $callableNode;
}
public function createSectionBeginNode(TitleNode $titleNode): SectionBeginNode
{
$sectionBeginNode = $this->create(NodeTypes::SECTION_BEGIN, [$titleNode]);
assert($sectionBeginNode instanceof SectionBeginNode);
return $sectionBeginNode;
}
public function createSectionEndNode(TitleNode $titleNode): SectionEndNode
{
$sectionEndNode = $this->create(NodeTypes::SECTION_END, [$titleNode]);
assert($sectionEndNode instanceof SectionEndNode);
return $sectionEndNode;
}
/** @param mixed[] $arguments */
private function create(string $type, array $arguments): Node
{
$node = $this->getNodeInstantiator($type)->create($arguments);
$this->eventManager->dispatchEvent(
PostNodeCreateEvent::POST_NODE_CREATE,
new PostNodeCreateEvent($node)
);
return $node;
}
private function getNodeInstantiator(string $type): NodeInstantiator
{
if (! isset($this->nodeInstantiators[$type])) {
throw new InvalidArgumentException(sprintf('Could not find node instantiator of type %s', $type));
}
return $this->nodeInstantiators[$type];
}
}

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\NodeFactory;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\AnchorNode;
use Doctrine\RST\Nodes\BlockNode;
use Doctrine\RST\Nodes\CallableNode;
use Doctrine\RST\Nodes\CodeNode;
use Doctrine\RST\Nodes\DefinitionListNode;
use Doctrine\RST\Nodes\DocumentNode;
use Doctrine\RST\Nodes\DummyNode;
use Doctrine\RST\Nodes\FigureNode;
use Doctrine\RST\Nodes\ImageNode;
use Doctrine\RST\Nodes\ListNode;
use Doctrine\RST\Nodes\MainNode;
use Doctrine\RST\Nodes\MetaNode;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Nodes\ParagraphNode;
use Doctrine\RST\Nodes\QuoteNode;
use Doctrine\RST\Nodes\RawNode;
use Doctrine\RST\Nodes\SectionBeginNode;
use Doctrine\RST\Nodes\SectionEndNode;
use Doctrine\RST\Nodes\SeparatorNode;
use Doctrine\RST\Nodes\SpanNode;
use Doctrine\RST\Nodes\TableNode;
use Doctrine\RST\Nodes\TitleNode;
use Doctrine\RST\Nodes\TocNode;
use Doctrine\RST\Nodes\WrapperNode;
use Doctrine\RST\Parser;
use Doctrine\RST\Parser\DefinitionList;
use Doctrine\RST\Parser\LineChecker;
use Doctrine\RST\Parser\ListItem;
use Doctrine\RST\Parser\TableSeparatorLineConfig;
interface NodeFactory
{
public function createDocumentNode(Environment $environment): DocumentNode;
/**
* @param string[] $files
* @param string[] $options
*/
public function createTocNode(Environment $environment, array $files, array $options): TocNode;
public function createTitleNode(Node $value, int $level, string $token): TitleNode;
public function createSeparatorNode(int $level): SeparatorNode;
/** @param string[] $lines */
public function createBlockNode(array $lines): BlockNode;
/** @param string[] $lines */
public function createCodeNode(array $lines): CodeNode;
public function createQuoteNode(DocumentNode $documentNode): QuoteNode;
public function createParagraphNode(SpanNode $span): ParagraphNode;
public function createAnchorNode(?string $value = null): AnchorNode;
/** @param ListItem[] $items */
public function createListNode(array $items, bool $ordered): ListNode;
public function createTableNode(TableSeparatorLineConfig $separatorLineConfig, string $type, LineChecker $lineChecker): TableNode;
/** @param string|string[]|SpanNode $span */
public function createSpanNode(Parser $parser, $span): SpanNode;
public function createDefinitionListNode(DefinitionList $definitionList): DefinitionListNode;
public function createWrapperNode(?Node $node, string $before = '', string $after = ''): WrapperNode;
public function createFigureNode(ImageNode $image, ?Node $document = null): FigureNode;
/** @param string[] $options */
public function createImageNode(string $url, array $options = []): ImageNode;
public function createMetaNode(string $key, string $value): MetaNode;
public function createRawNode(string $value): RawNode;
/** @param mixed[] $data */
public function createDummyNode(array $data): DummyNode;
public function createMainNode(): MainNode;
public function createCallableNode(callable $callable): CallableNode;
public function createSectionBeginNode(TitleNode $titleNode): SectionBeginNode;
public function createSectionEndNode(TitleNode $titleNode): SectionEndNode;
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\NodeFactory;
use Doctrine\Common\EventManager;
use Doctrine\RST\Environment;
use Doctrine\RST\Nodes\Node;
use Doctrine\RST\Nodes\NodeTypes;
use Doctrine\RST\Renderers\NodeRendererFactory;
use InvalidArgumentException;
use function assert;
use function in_array;
use function is_subclass_of;
use function sprintf;
class NodeInstantiator
{
/** @var string */
private $type;
/** @var string */
private $className;
/** @var NodeRendererFactory|null */
private $nodeRendererFactory;
/** @var EventManager|null */
private $eventManager;
/** @var Environment */
private $environment;
public function __construct(
string $type,
string $className,
Environment $environment,
?NodeRendererFactory $nodeRendererFactory = null,
?EventManager $eventManager = null
) {
if (! in_array($type, NodeTypes::NODES, true)) {
throw new InvalidArgumentException(
sprintf('Node type %s is not a valid node type.', $type)
);
}
if (! is_subclass_of($className, Node::class)) {
throw new InvalidArgumentException(
sprintf('%s class is not a subclass of %s', $className, Node::class)
);
}
$this->type = $type;
$this->className = $className;
$this->nodeRendererFactory = $nodeRendererFactory;
$this->eventManager = $eventManager;
$this->environment = $environment;
}
public function getType(): string
{
return $this->type;
}
/** @param mixed[] $arguments */
public function create(array $arguments): Node
{
$node = new $this->className(...$arguments);
assert($node instanceof Node);
if ($this->nodeRendererFactory !== null) {
$node->setNodeRendererFactory($this->nodeRendererFactory);
}
if ($this->eventManager !== null) {
$node->setEventManager($this->eventManager);
}
$node->setEnvironment($this->environment);
return $node;
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
class AnchorNode extends Node
{
/** @var string */
protected $value;
public function __construct(string $value)
{
parent::__construct($value);
}
public function getValue(): string
{
return $this->value;
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
final class BlockNode extends Node
{
/** @var string */
protected $value;
/** @param string[] $lines */
public function __construct(array $lines)
{
parent::__construct($this->normalizeLines($lines));
}
public function getValue(): string
{
return $this->value;
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
final class CallableNode extends Node
{
/** @var callable */
private $callable;
public function __construct(callable $callable)
{
parent::__construct();
$this->callable = $callable;
}
public function getCallable(): callable
{
return $this->callable;
}
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
/**
* Represents a "code node", which *sometimes* encompasses more than "code blocks".
*
* The intention of this class is for it to be used for "code blocks".
* However, if a directive returns true from wantCode(), they will
* be passed a CodeNode.
*/
class CodeNode extends Node
{
/** @var string */
protected $value;
/** @var bool */
private $raw = false;
/** @var string|null */
private $language = null;
/** @var string[] */
private $options = [];
/** @param string[] $lines */
public function __construct(array $lines)
{
parent::__construct($this->normalizeLines($lines));
}
public function getValue(): string
{
return $this->value;
}
public function setLanguage(?string $language = null): void
{
$this->language = $language;
}
public function getLanguage(): ?string
{
return $this->language;
}
public function setRaw(bool $raw): void
{
$this->raw = $raw;
}
public function isRaw(): bool
{
return $this->raw;
}
/** @param string[] $options */
public function setOptions(array $options = []): void
{
$this->options = $options;
}
/** @return string[] */
public function getOptions(): array
{
return $this->options;
}
}

View file

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
use Doctrine\RST\Parser\DefinitionList;
final class DefinitionListNode extends Node
{
/** @var DefinitionList */
private $definitionList;
public function __construct(DefinitionList $definitionList)
{
parent::__construct();
$this->definitionList = $definitionList;
}
public function getDefinitionList(): DefinitionList
{
return $this->definitionList;
}
}

View file

@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
use Doctrine\RST\Configuration;
use Doctrine\RST\Environment;
use Doctrine\RST\ErrorManager;
use Doctrine\RST\Renderers\FullDocumentNodeRenderer;
use Exception;
use function array_unshift;
use function assert;
use function count;
use function is_string;
use function sprintf;
class DocumentNode extends Node
{
/** @var Environment */
protected $environment;
/** @var Configuration */
private $configuration;
/** @var ErrorManager */
private $errorManager;
/** @var Node[] */
private $headerNodes = [];
/** @var Node[] */
private $nodes = [];
public function __construct(Environment $environment)
{
parent::__construct();
$this->environment = $environment;
$this->configuration = $environment->getConfiguration();
$this->errorManager = $environment->getErrorManager();
}
public function getEnvironment(): Environment
{
return $this->environment;
}
public function getConfiguration(): Configuration
{
return $this->configuration;
}
/** @return Node[] */
public function getHeaderNodes(): array
{
return $this->headerNodes;
}
public function renderDocument(): string
{
$renderedDocument = $this->doRenderDocument();
$this->postRenderValidate();
return $renderedDocument;
}
/** @return Node[] */
public function getNodes(?callable $function = null): array
{
$nodes = [];
if ($function === null) {
return $this->nodes;
}
foreach ($this->nodes as $node) {
if (! $function($node)) {
continue;
}
$nodes[] = $node;
}
return $nodes;
}
public function getTitle(): ?string
{
foreach ($this->nodes as $node) {
if ($node instanceof TitleNode && $node->getLevel() === 1) {
return $node->getValue()->render() . '';
}
}
return null;
}
/** @return mixed[] */
public function getTocs(): array
{
$tocs = [];
$nodes = $this->getNodes(static function ($node): bool {
return $node instanceof TocNode;
});
foreach ($nodes as $toc) {
assert($toc instanceof TocNode);
$files = $toc->getFiles();
foreach ($files as &$file) {
$file = $this->environment->canonicalUrl($file);
}
$tocs[] = $files;
}
return $tocs;
}
/** @return string[][] */
public function getTitles(): array
{
$titles = [];
$levels = [&$titles];
foreach ($this->nodes as $node) {
if (! ($node instanceof TitleNode)) {
continue;
}
$level = $node->getLevel();
$text = $node->getValue()->getText();
$redirection = $node->getTarget();
$value = $redirection !== '' ? [$text, $redirection] : $text;
if (! isset($levels[$level - 1])) {
continue;
}
$parent = &$levels[$level - 1];
$element = [$value, []];
$parent[] = $element;
$levels[$level] = &$parent[count($parent) - 1][1];
}
return $titles;
}
/** @param string|Node $node */
public function addNode($node): void
{
if (is_string($node)) {
$node = new RawNode($node);
}
$this->nodes[] = $node;
}
public function prependNode(Node $node): void
{
array_unshift($this->nodes, $node);
}
public function addHeaderNode(Node $node): void
{
$this->headerNodes[] = $node;
}
public function addCss(string $css): void
{
$css = $this->environment->relativeUrl($css);
if ($css === null) {
throw new Exception(sprintf('Could not get relative url for css %s', $css));
}
$this->addHeaderNode($this->environment->getNodeFactory()->createRawNode(
$this->environment->getTemplateRenderer()->render('stylesheet-link.html.twig', ['css' => $css])
));
}
public function addJs(string $js): void
{
$js = $this->environment->relativeUrl($js);
if ($js === null) {
throw new Exception(sprintf('Could not get relative url for js %s', $js));
}
$this->addHeaderNode($this->environment->getNodeFactory()->createRawNode(
$this->environment->getTemplateRenderer()->render('javascript.html.twig', ['js' => $js])
));
}
public function addFavicon(string $url = '/favicon.ico'): void
{
$url = $this->environment->relativeUrl($url);
if ($url === null) {
throw new Exception(sprintf('Could not get relative url for favicon %s', $url));
}
$this->addHeaderNode($this->environment->getNodeFactory()->createRawNode(
$this->environment->getTemplateRenderer()->render('favicon.html.twig', ['url' => $url])
));
}
protected function doRenderDocument(): string
{
$renderer = $this->getRenderer();
assert($renderer instanceof FullDocumentNodeRenderer);
return $renderer->renderDocument();
}
private function postRenderValidate(): void
{
if ($this->configuration->getIgnoreInvalidReferences() !== false) {
return;
}
$currentFileName = $this->environment->getCurrentFileName();
foreach ($this->environment->getInvalidLinks() as $invalidLink) {
$this->errorManager->error(
sprintf('Found invalid reference "%s"', $invalidLink->getName()),
$currentFileName
);
}
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
final class DummyNode extends Node
{
/** @var mixed[] */
public $data;
/** @param mixed[] $data */
public function __construct(array $data)
{
parent::__construct();
$this->data = $data;
}
protected function doRender(): string
{
return '';
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
final class FigureNode extends Node
{
/** @var ImageNode */
private $image;
/** @var Node|null */
private $document;
public function __construct(ImageNode $image, ?Node $document = null)
{
parent::__construct();
$this->image = $image;
$this->document = $document;
}
public function getImage(): ImageNode
{
return $this->image;
}
public function getDocument(): ?Node
{
return $this->document;
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\RST\Nodes;
final class ImageNode extends Node
{
/** @var string */
private $url;
/** @var string[] */
private $options;
/** @param string[] $options */
public function __construct(string $url, array $options = [])
{
parent::__construct();
$this->url = $url;
$this->options = $options;
}
public function getUrl(): string
{
return $this->url;
}
/** @return string[] */
public function getOptions(): array
{
return $this->options;
}
}

Some files were not shown because too many files have changed in this diff Show more