Skip to content

[FEATURE] Support :lines: in literalinclude - #1352

Open
CybotTM wants to merge 4 commits into
phpDocumentor:mainfrom
netresearch:feature/literalinclude-lines
Open

[FEATURE] Support :lines: in literalinclude#1352
CybotTM wants to merge 4 commits into
phpDocumentor:mainfrom
netresearch:feature/literalinclude-lines

Conversation

@CybotTM

@CybotTM CybotTM commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Builds on #1351 and contains its two commits, so only the last two commits are new here — see the commit list. Please review #1351 first; I will rebase this one onto main once that is merged.

What this adds

The Sphinx option :lines: for literalinclude, so a region can also be selected by line number:

..  literalinclude:: Example.php
    :language: php
    :lines: 1,3-5,20-

Line numbers are 1 based, ranges are inclusive, an omitted end means "up to the last line", and overlapping ranges are included once, in document order. Both ends are clamped to the lines that exist, so :lines: 1-99999999999 selects the whole region instead of iterating over the number the author wrote down.

The accepted spelling is validated with DefaultCodeNodeOptionMapper::LINE_NUMBER_RANGES_REGEX, the pattern already used for :emphasize-lines:, so both options are written the same way. If you would rather not have the directive reference the mapper's constant, say so and I will move the pattern to a shared place.

Order of application

Like Sphinx, :lines: is applied after :start-after: and :end-before:, so the numbers count within the marked region rather than within the file. There is an integration test for that combination.

I would still describe markers as the more valuable half of this: line numbers break on every edit above the selected region, markers do not. :lines: is here for Sphinx compatibility and for files that have no natural marker.

Behaviour on an invalid or empty selection

Each range that selects no line is warned about on its own, naming that range, and the remaining ranges are still included — :lines: 1-2,100 on a 15 line file includes lines 1 and 2 and says that 100 selected nothing. A missing value or a value that is not a list of line numbers logs a warning and includes nothing, consistent with a marker that is not found in #1351.

Documentation

The reference page added in #1351 gets a section on selecting lines by number, including the order of application and the advice to prefer markers where a file has them.

Tests

Eight integration tests: a mixed range list, an open ended range, an invalid specification, a range beyond the end of the file, a range list where only one entry is beyond the end, :lines: used without a value, a range far beyond PHP_INT_MAX territory as a regression test for the clamping, and :lines: combined with markers.

Local run of phpunit (unit, functional, integration), phpcs, phpstan and deptrac: green.

Assisted by claude-code:claude-opus-5 — Session

@CybotTM
CybotTM force-pushed the feature/literalinclude-lines branch 3 times, most recently from 23bcfbc to 5e05cbe Compare August 15, 2026 12:50
`literalinclude` always included the complete referenced file. Authors who
want to show one relevant region of a real source or configuration file had
to duplicate that region into a dedicated snippet file, which then silently
drifts away from the original.

Add the Sphinx options `:start-after:` and `:end-before:`. The included
region starts on the line following the first line containing the
`start-after` text and ends on the line preceding the first line containing
the `end-before` text. `end-before` is searched behind the start of the
region, so the same marker text may be used more than once in a file.

Markers are stable against edits outside the marked region, unlike line
numbers.

Nothing is included, and a warning names the reason, when a marker is not
found, when an option is used without a value, when `end-before` occurs only
above the line matched by `start-after`, and when the marked region is empty.
Falling back to the complete file in those cases would publish exactly the
content the option was meant to exclude.

The selection happens before the CodeNode is created, so it stays in the
directive rather than in DefaultCodeNodeOptionMapper, which is shared with
`code-block` and only maps display options.

The logger is an optional constructor argument, as in ConfvalDirective, so
that adding it does not break consumers that build the directive themselves.

Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_015QXXkquh2eQNBiTYA39Wss
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
The reference section had no page for `literalinclude`, so neither the
directive itself nor its options were documented anywhere.

Add one, covering the directive, the new `:start-after:` and `:end-before:`
options, and the options the shared code node option mapper provides. Every
documented behaviour was rendered and read back before being written down,
including what happens when the file is missing (error, nothing rendered) and
when a marker is not found (warning, nothing included).

Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_015QXXkquh2eQNBiTYA39Wss
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Add the Sphinx option `:lines:` to `literalinclude`, so a region can also be
selected by line number, for example `:lines: 1,3-5,20-`. Line numbers are 1
based, ranges are inclusive, an omitted end means "up to the last line", and
overlapping ranges are included once, in document order.

The accepted spelling is the one already used by `:emphasize-lines:`, so both
options are written the same way.

Like Sphinx, `:lines:` is applied after `:start-after:` and `:end-before:`,
therefore the numbers count within the marked region rather than within the
file. Markers stay the more robust choice, because line numbers break on
every edit above the selected region.

Both ends of a range are clamped to the lines that exist, so a range far
beyond the end of the file selects nothing instead of iterating over the
numbers the author wrote down. Each range that selects no line is warned
about on its own, naming the range, while the remaining ranges are still
included. A missing value, or a value that is not a list of line numbers,
logs a warning and includes nothing.

Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_015QXXkquh2eQNBiTYA39Wss
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
Add the `:lines:` option to the reference page: syntax, that overlapping
ranges are included once in the order of the file, that it is applied after
the markers and therefore counts within the marked region, and what happens
when the value selects nothing.

Each of these was rendered and read back before being written down.

Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_015QXXkquh2eQNBiTYA39Wss
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
@linawolf

Copy link
Copy Markdown
Contributor

Coverage gap: there are existing tests for :linenos:/:lineno-start: in isolation, but none combine it with the new :lines:/:start-after:/:end-before: filtering. Checked against Sphinx's LiteralIncludeReader -- :lineno-start: is meant to be a plain override independent of filtering (no :lineno-match: equivalent exists here), so I believe current behavior is already correct, just untested. Worth adding one fixture combining them, e.g. :start-after: + :lines: + :lineno-start: 10, asserting the output numbers from 10 regardless of where the selected content sat in the source file.

@linawolf

Copy link
Copy Markdown
Contributor

literalinclude-lines-huge-range uses :lines: 1-99999999999 (~10^11) as the regression test for clamping "far beyond PHP_INT_MAX territory," but that value doesn't actually exceed PHP_INT_MAX on 64-bit systems (~9.2x10^18) -- it's off by about 7 orders of magnitude. A genuinely overflowing value clamps correctly today (checked manually), but that path has no test coverage, so a future change to parseRange()'s arithmetic could silently reintroduce overflow behavior undetected. Suggest bumping the value to something that actually exceeds PHP_INT_MAX on 64-bit systems, e.g. 1-99999999999999999999.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants