diff --git a/CHANGELOG.md b/CHANGELOG.md
index f631cb1..1e8049c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## [Unreleased]
+
+- Add inclusive and exclusive literal line-boundary selectors.
+
## [0.7.0] - 2026-09-20
- Extract immutable source-code slices by line, text, or language structure.
diff --git a/README.md b/README.md
index b9bb25b..5e5d953 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,14 @@ $slice = $source->slice()
->before('// example:end');
```
+Line-boundary selectors include or exclude complete matching lines:
+
+```php
+$slice = $source->slice()
+ ->afterLine('')
+ ->beforeLine('');
+```
+
Missing boundaries and attempts to expand a slice throw explicit exceptions rather than returning
an approximate result.
diff --git a/docs/languages/index.md b/docs/languages/index.md
index ca41e08..dcaa548 100644
--- a/docs/languages/index.md
+++ b/docs/languages/index.md
@@ -11,6 +11,8 @@ and literal-text selection.
## Text and lines
+[Text](text.md) covers line ranges and literal line boundaries for HTML, SVG, YAML, Markdown, environment files, and unknown source types.
+
HTML, SVG, YAML, Markdown, environment files, and unknown source types remain
ordinary text. Select an inclusive range of original lines with `lines()`, or
chain `after()` and `before()` around known literal boundaries. These selectors
diff --git a/docs/languages/text.md b/docs/languages/text.md
new file mode 100644
index 0000000..49ac519
--- /dev/null
+++ b/docs/languages/text.md
@@ -0,0 +1,259 @@
+# Text
+
+Use original line numbers for a fixed range, or literal source text for known
+boundaries. Prefer a structural selector when a supported declaration has a
+stable name. HTML, SVG, YAML, Markdown,
+and environment files can all be sliced this way; no comment markers need to be added.
+
+| Selector | Boundary behavior |
+| --- | --- |
+| `fromLine($text)` | Start at the first matching line, including it. |
+| `throughLine($text)` | End at the first matching line, including its content. |
+| `afterLine($text)` | Start after the first matching line. |
+| `beforeLine($text)` | End before the first matching line. |
+
+All four search case-sensitive substrings inside the current slice. They do not match nested
+HTML elements, YAML mappings, or Markdown sections structurally. Use `method()`, `rule()`, or
+`block()` when the source language provides them.
+
+The PHP selections below assume [installation](../installation.md) is complete
+and Composer's autoloader is loaded. Save each input under the filename shown before running its PHP selection.
+Choose these selectors when you know the exact text or original line numbers.
+
+## Select exact source lines
+
+This standalone example selects one original source line without relying on
+language detection:
+
+```php
+lines(2, 2)->content(), "\n";
+```
+
+Save it beside `vendor`, then run it with PHP. It prints:
+
+```text
+second
+```
+
+Use `after()` and `before()` for boundaries within a line; use the line helpers
+below when the matching lines themselves should be included or excluded.
+
+## HTML: include a complete form
+
+**Source: `checkout.html`**
+
+```html
+
+
+
');
+echo $slice->content();
+```
+
+**Result**
+
+```html
+
+```
+
+To keep only its contents, exclude the matching tag lines:
+
+**Selection**
+
+```php
+use Alto\Code\Slicer\CodeSource;
+
+$slice = CodeSource::fromFile('checkout.html')->slice()
+ ->afterLine('');
+echo $slice->content();
+```
+
+**Result**
+
+```html
+
+
+```
+
+## SVG: select a group
+
+**Source: `confirmed.svg`**
+
+```svg
+
+```
+
+**Selection**
+
+```php
+use Alto\Code\Slicer\CodeSource;
+
+$slice = CodeSource::fromFile('confirmed.svg')->slice()
+ ->fromLine('throughLine('');
+echo $slice->content();
+```
+
+**Result**
+
+```svg
+
+
+
+
+```
+
+This group has no nested `g`. With nested groups, `throughLine('')` would stop at the first
+closing line, so it cannot stand in for an element selector.
+
+## YAML: select known settings
+
+**Source: `checkout.yaml`**
+
+```yaml
+app:
+ name: Checkout
+ currency: EUR
+services:
+ App\Checkout:
+ arguments:
+ $taxRate: 1.2
+ $currency: '%app.currency%'
+receipt:
+ subject: Your order is confirmed
+```
+
+**Selection**
+
+```php
+use Alto\Code\Slicer\CodeSource;
+
+$slice = CodeSource::fromFile('checkout.yaml')->slice()
+ ->fromLine('services:')
+ ->beforeLine('receipt:');
+echo $slice->content();
+```
+
+**Result**
+
+```yaml
+services:
+ App\Checkout:
+ arguments:
+ $taxRate: 1.2
+ $currency: '%app.currency%'
+```
+
+The keys are text boundaries. This selection does not infer YAML indentation or resolve a key path.
+
+## Markdown: select a section between headings
+
+**Source: `checkout.md`**
+
+```markdown
+# Checkout
+
+## Installation
+
+Install the package with Composer.
+
+## Usage
+
+Load your source and select a method.
+```
+
+**Selection**
+
+```php
+use Alto\Code\Slicer\CodeSource;
+
+$slice = CodeSource::fromFile('checkout.md')->slice()
+ ->fromLine('## Installation')
+ ->beforeLine('## Usage');
+echo $slice->content();
+```
+
+**Result**
+
+```markdown
+## Installation
+
+Install the package with Composer.
+
+```
+
+The blank line before the next heading is preserved. Both heading texts are supplied explicitly.
+
+## Environment files: select variable assignments
+
+**Source: `.env.example`**
+
+```dotenv
+APP_ENV=dev
+APP_NAME="Checkout demo"
+APP_URL=http://localhost:8000
+ASSET_URL=${APP_URL}/assets
+MAILER_DSN=null://null
+```
+
+**Selection**
+
+```php
+use Alto\Code\Slicer\CodeSource;
+
+$slice = CodeSource::fromFile('.env.example', 'dotenv')->slice()
+ ->fromLine('APP_NAME=')
+ ->throughLine('ASSET_URL=');
+echo $slice->content();
+```
+
+**Result**
+
+```dotenv
+APP_NAME="Checkout demo"
+APP_URL=http://localhost:8000
+ASSET_URL=${APP_URL}/assets
+```
+
+The source is read as text. Environment variables are not loaded or expanded.
+
+## Exact lines and text within a line
+
+`lines(4, 8)` selects inclusive line numbers from the original file. `after('return ')` and
+`before(';')` cut at exact text inside a line and exclude that text. These methods also work
+when the source language is unknown.
+
+Every selection retains the original bytes and source line numbers. Empty or multiline search
+text is rejected by the line helpers; missing text throws `SourceTextNotFound`.
+
+See the [public API](../selectors.md) or return to the [language guides](index.md).
diff --git a/docs/selectors.md b/docs/selectors.md
index 12c5cef..84bcae9 100644
--- a/docs/selectors.md
+++ b/docs/selectors.md
@@ -43,12 +43,16 @@ case-sensitive occurrence fully contained in the current slice.
| `lines(int $start, int $end): CodeSlice` | Select inclusive original source lines. Their complete range must fit inside the current slice. |
| `after(string $text): CodeSlice` | Keep bytes after the matched text; exclude the match. |
| `before(string $text): CodeSlice` | Keep bytes before the matched text; exclude the match. |
+| `fromLine(string $text): CodeSlice` | Start at the first line containing the text; include that line. |
+| `throughLine(string $text): CodeSlice` | End at the first line containing the text; include that line. |
+| `afterLine(string $text): CodeSlice` | Start after the first line containing the text. |
+| `beforeLine(string $text): CodeSlice` | End before the first line containing the text. |
`lines()` throws `InvalidSourceRange` for invalid line numbers or a range
outside the current slice. Text selectors throw `SourceTextNotFound` when no
complete match is found. Empty search text throws `InvalidArgumentException`.
-See [Languages](languages/index.md#text-and-lines) for a complete chained example.
+See [Text](languages/text.md) for complete HTML, SVG, YAML, Markdown, and environment-file examples.
## Select declarations
diff --git a/src/CodeSlice.php b/src/CodeSlice.php
index be180ec..3016a22 100644
--- a/src/CodeSlice.php
+++ b/src/CodeSlice.php
@@ -84,6 +84,69 @@ public function before(string $text): self
return $this->withRange(new SourceRange($this->range->start, $position));
}
+ /**
+ * Keep the content after the first line containing the text, excluding its line ending.
+ */
+ public function afterLine(string $text): self
+ {
+ $line = $this->matchingLine($text);
+ $start = $line < $this->source->lineCount()
+ ? $this->source->rangeForLines($line + 1, $line + 1)->start
+ : $this->range->end;
+
+ return $this->withRange(new SourceRange(min($start, $this->range->end), $this->range->end));
+ }
+
+ /**
+ * Keep the content before the first line containing the text, excluding the preceding line ending.
+ */
+ public function beforeLine(string $text): self
+ {
+ $line = $this->matchingLine($text);
+ $end = $line > 1
+ ? $this->source->rangeForLines($line - 1, $line - 1)->end
+ : $this->range->start;
+
+ return $this->withRange(new SourceRange($this->range->start, max($end, $this->range->start)));
+ }
+
+ /**
+ * Keep the content from the first line containing the text, including that line.
+ */
+ public function fromLine(string $text): self
+ {
+ $line = $this->matchingLine($text);
+ $start = $this->source->rangeForLines($line, $line)->start;
+
+ return $this->withRange(new SourceRange(max($start, $this->range->start), $this->range->end));
+ }
+
+ /**
+ * Keep the content through the first line containing the text, excluding its line ending.
+ */
+ public function throughLine(string $text): self
+ {
+ $line = $this->matchingLine($text);
+ $end = $this->source->rangeForLines($line, $line)->end;
+
+ return $this->withRange(new SourceRange($this->range->start, min($end, $this->range->end)));
+ }
+
+ private function matchingLine(string $text): int
+ {
+ if ('' === $text || str_contains($text, "\n") || str_contains($text, "\r")) {
+ throw new \InvalidArgumentException('The searched text must be non-empty and contain no line breaks.');
+ }
+
+ $position = strpos($this->source->content(), $text, $this->range->start);
+
+ if (false === $position || $position + strlen($text) > $this->range->end) {
+ throw SourceTextNotFound::forText($text);
+ }
+
+ return $this->source->lineAtOffset($position);
+ }
+
public function beforeNextClass(): self
{
$class = $this->classes()->nextClass($this->range);
diff --git a/tests/LineBoundaryTest.php b/tests/LineBoundaryTest.php
new file mode 100644
index 0000000..ce59436
--- /dev/null
+++ b/tests/LineBoundaryTest.php
@@ -0,0 +1,199 @@
+',
+ ' ',
+ '