Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/reference/restructuredtext/literalinclude.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.. include:: /include.rst.txt

.. _literalinclude:

==============
Literalinclude
==============

The ``literalinclude`` directive includes the content of another file as a code block. It lets the documentation
show a real, working file instead of a copy that has to be kept in sync by hand.

.. code-block::

.. literalinclude:: Example.php
:language: php

The path is resolved relative to the document that contains the directive, a path starting with ``/`` relative to
the root of the documentation. If the file cannot be read, an error is logged and nothing is rendered in its place.
Rendering with ``--fail-on-error`` turns that error into a failing build.

Including only a part of a file
===============================

Often only one region of a file is worth showing. The options ``:start-after:`` and ``:end-before:`` select that
region by the text of the lines enclosing it:

.. code-block::

.. literalinclude:: Example.php
:language: php
:start-after: // begin example
:end-before: // end example

The region starts on the line *following* the first line that contains the ``start-after`` text, and ends on the
line *preceding* the first line that contains the ``end-before`` text. Both marker lines are left out. The
``end-before`` text is searched behind the start of the region, so the same marker may occur several times in one
file.

Either option can be used on its own: ``start-after`` alone includes everything down to the end of the file,
``end-before`` alone everything from the beginning of the file.

Markers remain correct when the file is edited above or below the selected region. That is why they are the better
choice for a file that is under active development.

If no line contains the given text, a warning is logged and nothing is included, so that the option cannot silently
publish the very content it was meant to exclude. The same happens when the marked region turns out to be empty, or
when the ``end-before`` text occurs only above the line matched by ``start-after``. Rendering with ``--fail-on-log``
turns any of these warnings into a failing build.

Options
=======

``:language:``
Language used for syntax highlighting, for example ``php``, ``bash`` or ``yaml``.

``:caption:``
Caption rendered above the code block. Inline markup such as ``**bold**`` is allowed.

``:start-after:``
Text of the line the included region starts after. The line itself is not included.

``:end-before:``
Text of the line the included region ends before. The line itself is not included.

``:emphasize-lines:``
Line numbers to be highlighted, for example ``3,5-6``. Counted within the included region.

``:linenos:``
Displays line numbers, starting at 1.

``:lineno-start:``
First line number to display. Also switches the numbering on.

``:number-lines:``
Displays line numbers. Takes the first line number as an optional value.
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@
use phpDocumentor\Guides\RestructuredText\Directives\OptionMapper\CodeNodeOptionMapper;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
use Psr\Log\LoggerInterface;
use RuntimeException;

use function array_slice;
use function count;
use function explode;
use function is_string;
use function sprintf;
use function str_contains;

final class LiteralincludeDirective extends BaseDirective
{
public function __construct(private readonly CodeNodeOptionMapper $codeNodeOptionMapper)
{
public function __construct(
private readonly CodeNodeOptionMapper $codeNodeOptionMapper,
private readonly LoggerInterface|null $logger = null,
) {
}

public function getName(): string
Expand Down Expand Up @@ -56,9 +63,135 @@ public function processNode(
throw new RuntimeException(sprintf('Could not load file from path %s', $path));
}

$codeNode = new CodeNode(explode("\n", $contents));
$lines = $this->selectLines(explode("\n", $contents), $directive, $blockContext);

$codeNode = new CodeNode($lines);
$this->codeNodeOptionMapper->apply($codeNode, $directive->getOptions(), $blockContext);

return $codeNode;
}

/**
* Reduces the included file to the region enclosed by the ``start-after`` and ``end-before`` markers.
*
* The region starts on the line following the first line containing the ``start-after`` marker and ends
* on the line preceding the first line containing the ``end-before`` marker. The ``end-before`` marker
* is searched behind the start of the region, so the same marker text may be used more than once in a file.
*
* @param string[] $lines
*
* @return string[]
*/
private function selectLines(array $lines, Directive $directive, BlockContext $blockContext): array
{
$start = 0;
$end = count($lines);

if ($directive->hasOption('start-after')) {
$marker = $this->optionValue($directive, 'start-after', $blockContext);
if ($marker === null) {
return [];
}

$lineNumber = $this->findMarker($lines, $marker, $start);
if ($lineNumber === null) {
$this->warnMarkerNotFound($directive, 'start-after', $marker, $blockContext);

return [];
}

$start = $lineNumber + 1;
}

if ($directive->hasOption('end-before')) {
$marker = $this->optionValue($directive, 'end-before', $blockContext);
if ($marker === null) {
return [];
}

$lineNumber = $this->findMarker($lines, $marker, $start);
if ($lineNumber === null) {
if ($this->findMarker($lines, $marker, 0) === null) {
$this->warnMarkerNotFound($directive, 'end-before', $marker, $blockContext);
} else {
$this->logger?->warning(
sprintf(
'Option ":end-before:" of directive "literalinclude": "%s" occurs in "%s" only above the line matched by ":start-after:", nothing was included.',
$marker,
$directive->getData(),
),
$blockContext->getLoggerInformation(),
);
}

return [];
}

$end = $lineNumber;
}

$selection = array_slice($lines, $start, $end - $start);

if ($selection === []) {
$this->logger?->warning(
sprintf(
'Directive "literalinclude": the region marked in "%s" is empty, nothing was included.',
$directive->getData(),
),
$blockContext->getLoggerInformation(),
);
}

return $selection;
}

/** Returns the text of an option, or null if the option was used without a usable value. */
private function optionValue(Directive $directive, string $option, BlockContext $blockContext): string|null
{
$value = $directive->getOption($option)->getValue();
if (!is_string($value) || $value === '') {
$this->logger?->warning(
sprintf('Option ":%s:" of directive "literalinclude" requires a value, nothing was included.', $option),
$blockContext->getLoggerInformation(),
);

return null;
}

return $value;
}

/**
* Returns the number of the first line at or behind $offset that contains $marker, or null if there is none.
*
* @param string[] $lines
*/
private function findMarker(array $lines, string $marker, int $offset): int|null
{
$lineCount = count($lines);
for ($lineNumber = $offset; $lineNumber < $lineCount; $lineNumber++) {
if (str_contains($lines[$lineNumber], $marker)) {
return $lineNumber;
}
}

return null;
}

private function warnMarkerNotFound(
Directive $directive,
string $option,
string $marker,
BlockContext $blockContext,
): void {
$this->logger?->warning(
sprintf(
'Option ":%s:" of directive "literalinclude": no line containing "%s" was found in "%s", nothing was included.',
$option,
$marker,
$directive->getData(),
),
$blockContext->getLoggerInformation(),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- content start -->
<div class="section" id="index">
<h1>index</h1>
<pre><code class="language-php">&lt;?php

declare(strict_types=1);

class Example
{
// begin example
public function test(): string
{
return &#039;this is a test&#039;;
}
</code></pre>

</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

class Example
{
// begin example
public function test(): string
{
return 'this is a test';
}

// end example
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
index
=====

.. literalinclude:: _code/_Example.php
:language: php
:end-before: // end example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- content start -->
<div class="section" id="index">
<h1>index</h1>
<pre><code class="language-php"></code></pre>

</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Directive "literalinclude": the region marked in "_code/_Example.php" is empty, nothing was included.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

class Example
{
// begin example
// end example
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
index
=====

.. literalinclude:: _code/_Example.php
:language: php
:start-after: // begin example
:end-before: // end example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- content start -->
<div class="section" id="index">
<h1>index</h1>
<pre><code class="language-php"></code></pre>

</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Option ":start-after:" of directive "literalinclude": no line containing "// this marker does not exist" was found in "_code/_Example.php", nothing was included.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

class Example
{
// begin example
public function test(): string
{
return 'this is a test';
}

// end example
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
index
=====

.. literalinclude:: _code/_Example.php
:language: php
:start-after: // this marker does not exist
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- content start -->
<div class="section" id="index">
<h1>index</h1>
<pre><code class="language-php"></code></pre>

</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Option ":end-before:" of directive "literalinclude": "// begin example" occurs in "_code/_Example.php" only above the line matched by ":start-after:", nothing was included.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

class Example
{
// begin example
public function test(): string
{
return 'this is a test';
}

// end example
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
index
=====

.. literalinclude:: _code/_Example.php
:language: php
:start-after: // end example
:end-before: // begin example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- content start -->
<div class="section" id="index">
<h1>index</h1>
<pre><code class="language-php"> public function test(): string
{
return &#039;this is a test&#039;;
}
</code></pre>

</div>
<!-- content end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

class Example
{
// begin example
public function test(): string
{
return 'this is a test';
}

// end example
}
Loading
Loading