Skip to content
Merged
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
46 changes: 26 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ALTO CodeSnippet
# ALTO Code Snippet

Represent immutable code snippets with source lines, selections, and presentation-neutral
annotations.
Expand All @@ -9,7 +9,7 @@ annotations.
  ![License](https://img.shields.io/github/license/altophp/code-snippet?label=License&labelColor=050608&color=00B7FF)
  [![GitHub Sponsors](https://img.shields.io/github/sponsors/smnandre?logo=githubsponsors&logoColor=00B7FF&label=%20Sponsor&labelColor=050608&color=00B7FF)](https://github.com/sponsors/smnandre)

CodeSnippet turns source code into a portable model with original line numbers, selected lines,
Code Snippet turns source code into a portable model with original line numbers, selected lines,
and generic byte-range annotations. Renderers can consume that model without coupling this package
to HTML, SVG, terminals, slides, or a syntax highlighter.

Expand All @@ -24,37 +24,38 @@ echo $snippet->lines()[2]->number; // 26

## Installation

Install ALTO CodeSnippet with Composer:
Code Snippet is currently distributed from its development branch. Add the
repository explicitly, then require `dev-main`:

```bash
composer require alto/code-snippet
composer config repositories.alto-code-snippet vcs https://github.com/altophp/code-snippet
composer require alto/code-snippet:dev-main
```

CodeSnippet requires PHP 8.4 or later and `alto/language`.
Code Snippet requires PHP 8.4 or later and installs `alto/language`.

## Quick Start
## Quick start

Create a snippet, select a line, and attach an application-defined annotation:
Create a snippet, keep its original source position, and select one line:

```php
use Alto\Code\Snippet\CodeAnnotation;
use Alto\Code\Snippet\CodeSnippet;

$snippet = CodeSnippet::fromCode(
"public function run(): void\n{\n execute();\n}",
'php',
sourceName: 'src/Runner.php',
startLine: 24,
)
->selectLines(3)
->annotate(new CodeAnnotation(
offset: 0,
length: 6,
type: 'syntax',
data: ['scope' => 'keyword'],
));

echo json_encode($snippet, JSON_THROW_ON_ERROR);
)->selectLines(3);

$line = $snippet->lines()[2];
printf("line=%d selected=%s %s\n", $line->number, $line->selected ? 'true' : 'false', $line->code);
```

The result is:

```text
line=26 selected=true execute();
```

Every transformation returns a new value. The original snippet remains unchanged.
Expand Down Expand Up @@ -126,10 +127,15 @@ $json = json_encode($snippet, JSON_THROW_ON_ERROR);

## Package boundary

CodeSnippet does not read files, detect languages, locate declarations, tokenize code, or render
Code Snippet does not read files, detect languages, locate declarations, tokenize code, or render
output. It only owns the immutable, presentation-neutral data model passed between those steps.

See the [documentation](docs/index.md) for installation, a guided example, and the public API.
## Documentation

- [Installation](docs/installation.md): install the development package and verify it.
- [Getting started](docs/getting-started.md): create a snippet and inspect a selected line.
- [Model](docs/model.md): work with snippets, lines, annotations, segments, and transformations.
- [Documentation index](docs/index.md): read the package overview and boundaries.

## Contributing

Expand Down
47 changes: 25 additions & 22 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
# Getting started

Create a snippet with its original source position, select lines, then attach generic byte-range
annotations:
After [installation](installation.md), save this as `snippet.php` beside
`vendor/`. It creates a snippet that starts on line 24 of its original file,
then selects its third line. The package does not read that file; the source
name is metadata.

```php
use Alto\Code\Snippet\CodeAnnotation;
<?php

require __DIR__.'/vendor/autoload.php';

use Alto\Code\Snippet\CodeSnippet;

$snippet = CodeSnippet::fromCode(
"public function run(): void\n{\n execute();\n}",
'php',
sourceName: 'src/Runner.php',
startLine: 24,
)
->selectLines(3)
->annotate(new CodeAnnotation(
offset: 0,
length: 6,
type: 'syntax',
data: ['scope' => 'keyword'],
));
```

`selectLines()` uses one-based positions inside the snippet. Every `CodeLine` also carries its
original source line number.
)->selectLines(3);

```php
$line = $snippet->lines()[2];
printf(
"index=%d number=%d selected=%s\n",
$line->index,
$line->number,
$line->selected ? 'true' : 'false',
);
echo $line->code, "\n";
```

Run `php snippet.php`. The output is:

$line->index; // 3
$line->number; // 26
$line->selected; // true
```text
index=3 number=26 selected=true
execute();
```

Annotations on the complete snippet use byte offsets relative to `code()`. Line annotations are
clipped and shifted to line-relative offsets. `segments()` then exposes contiguous text regions
with stable annotation sets.
`selectLines()` uses one-based positions inside the snippet. Selection marks a
line; it does not remove the other lines or define a visual effect. Continue
with the [model](model.md) to annotate, slice, indent, and export snippets.
28 changes: 20 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
# CodeSnippet
# Code Snippet

CodeSnippet represents code as immutable lines with source provenance, selected lines, and
presentation-neutral annotations.
Code Snippet stores code, original line numbers, selections, and annotations as
immutable values. Use it to pass an excerpt between extraction, analysis, and
presentation without tying the data to a renderer.

- [Installation](installation.md)
- [Getting started](getting-started.md)
- [Public API](public-api.md)
```php
use Alto\Code\Snippet\CodeSnippet;

The package owns the portable snippet model. It does not read files, locate declarations, tokenize
code, or render output.
$snippet = CodeSnippet::fromCode("first\nsecond\nthird", 'php', startLine: 24)
->selectLines(3);

echo $snippet->lines()[2]->number; // 26
```

## Documentation

- [Installation](installation.md): install the package and verify it can create a snippet.
- [Getting started](getting-started.md): create a snippet and inspect a selected line.
- [Model](model.md): work with snippets, lines, annotations, segments, and transformations.

The package owns the portable snippet model. It does not read files, detect
languages, locate declarations, tokenize code, or render output.
8 changes: 5 additions & 3 deletions docs/installation.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Installation

Install CodeSnippet with Composer:
Code Snippet is currently distributed from its development branch. Add the
repository explicitly, then require `dev-main`:

```bash
composer require alto/code-snippet
composer config repositories.alto-code-snippet vcs https://github.com/altophp/code-snippet
composer require alto/code-snippet:dev-main
```

CodeSnippet requires PHP 8.4 or later and `alto/language`.
Code Snippet requires PHP 8.4 or later and installs `alto/language`.

Create a snippet from code and an optional language slug:

Expand Down
164 changes: 164 additions & 0 deletions docs/model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Model

Code Snippet represents source text and presentation hints with four immutable
values. Transformations return a new value, so the original snippet remains
available to another consumer.

| Value | Role |
| --- | --- |
| `CodeSnippet` | Complete code, provenance, selections, and annotations |
| `CodeLine` | One line with snippet and original source coordinates |
| `CodeAnnotation` | Typed metadata attached to a byte range |
| `CodeSegment` | Contiguous line text with one stable annotation set |

All four values implement `JsonSerializable`.

## Snippets

Create a `CodeSnippet` from code. Supply a language when known, a source name
for provenance, and the original first line number when the excerpt came from a
larger file.

```php
use Alto\Code\Snippet\CodeSnippet;

$snippet = CodeSnippet::fromCode(
"one\ntwo\nthree",
'php',
sourceName: 'src/Example.php',
startLine: 20,
)->selectLines(2);
```

The code is stored verbatim, including LF, CRLF, or CR line endings. Read it
with `code()`, `language()`, `sourceName()`, `startLine()`, `endLine()`, and
`lineCount()`. An empty snippet has zero lines.

## Lines

`lines()` returns `CodeLine` values with two coordinate systems:

| Value | Coordinate system | Example |
| --- | --- | --- |
| `index` | One-based position inside the snippet | `2` |
| `number` | One-based position in the original source | `21` when `startLine` is `20` |
| `code` | Line content without its line break | `two` |
| `selected` | Presentation-neutral line marker | `true` |

```php
$line = $snippet->lines()[1];

printf(
"index=%d source=%d selected=%s code=%s\n",
$line->index,
$line->number,
$line->selected ? 'true' : 'false',
$line->code,
);
```

This prints `index=2 source=21 selected=true code=two`. Selected line numbers
must exist inside the snippet.

## Annotations

A `CodeAnnotation` attaches an application-defined type and optional metadata
to a half-open byte range: the start offset is included and the end offset is
excluded.

```php
use Alto\Code\Snippet\CodeAnnotation;

$annotated = $snippet->annotate(
new CodeAnnotation(4, 3, 'focus'),
new CodeAnnotation(5, 5, 'warning', ['label' => 'Review']),
);
```

Snippet offsets are relative to `code()`. `CodeLine::annotations()` clips an
annotation to the line and shifts its offset to the beginning of that line.
Annotations may overlap or cross line breaks.

```text
Snippet "abc\ndef": annotation [2, 5)
line 1 "abc": [2, 3) covers "c"
line 2 "def": [0, 1) covers "d"
```

Offsets use PHP byte semantics. For UTF-8, byte positions can differ from
character positions. Offsets must be non-negative, lengths positive, types
non-blank, and ranges contained in the snippet.

## Segments

`CodeLine::segments()` splits a line wherever its active annotations change.
Overlapping annotations remain active together; the consumer decides how to
combine their visual styles.

```php
$overlap = CodeSnippet::fromCode('abcd')->annotate(
new CodeAnnotation(0, 3, 'focus'),
new CodeAnnotation(1, 2, 'warning'),
);

foreach ($overlap->lines()[0]->segments() as $segment) {
$types = array_map(
static fn(CodeAnnotation $annotation): string => $annotation->type,
$segment->annotations,
);
printf("%d %s [%s]\n", $segment->offset, $segment->text, implode(',', $types));
}
```

The result is:

```text
0 a [focus]
1 bc [focus,warning]
3 d []
```

Segment offsets are line-relative. Unannotated text is still a segment; an
empty line has no segments.

## Transformations

`slice($start, $end)` projects a half-open byte range from an annotated
snippet. Crossing annotations are clipped and shifted. Original line numbers
and selections follow the remaining code.

```php
$slice = $snippet->slice(4, 7);
$line = $slice->lines()[0];
```

Here `$line->code` is `two`, its snippet index is `1`, its original source
number is `21`, and it remains selected.

Use `dedent()` or its `unindent()` alias to remove common indentation.
`indent($spaces)` adds spaces to non-empty lines. Both operations preserve
source line numbers and move annotations with the surviving text. Annotations
inside removed indentation disappear, and annotations crossing it are clipped.

## Export

`toArray()` exports code, language, provenance, selections, lines, and
annotations. `json_encode()` produces the same portable model through
`JsonSerializable`.

```php
$payload = $snippet->toArray();
$json = json_encode($snippet, JSON_THROW_ON_ERROR);
```

The package does not provide a JSON import factory or a renderer.
[Code Slicer](https://altophp.com/code-slicer) can locate source declarations,
[Code Highlight](https://altophp.com/code-highlight) can tokenize code, and
[Code Kit](https://altophp.com/code-kit) can connect those stages to this model.

## Invalid input

Invalid source positions, selected lines, slice ranges, annotation ranges,
language identifiers, or indentation values raise `InvalidArgumentException`.
Use `lineCount()` and `strlen($snippet->code())` to check the corresponding
line and byte boundaries before applying user-provided coordinates.
Loading
Loading