Skip to content

Commit

Permalink
feat(documentator)!: Markdown Extraction support (#220)
Browse files Browse the repository at this point in the history
  • Loading branch information
LastDragon-ru authored Jan 23, 2025
2 parents 33a6d8c + 2b922d3 commit ed14398
Show file tree
Hide file tree
Showing 50 changed files with 1,116 additions and 602 deletions.
4 changes: 2 additions & 2 deletions packages/documentator/src/Editor/Locations/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Traversable;

/**
* @implements IteratorAggregate<array-key, Coordinate>
* @implements IteratorAggregate<mixed, Coordinate>
*/
readonly class Location implements IteratorAggregate {
public function __construct(
Expand All @@ -23,7 +23,7 @@ public function __construct(
}

/**
* @return Traversable<array-key, Coordinate>
* @return Traversable<mixed, Coordinate>
*/
#[Override]
public function getIterator(): Traversable {
Expand Down
13 changes: 13 additions & 0 deletions packages/documentator/src/Markdown/Contracts/Extraction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\Documentator\Markdown\Contracts;

use LastDragon_ru\LaraASP\Documentator\Editor\Coordinate;
use LastDragon_ru\LaraASP\Documentator\Markdown\Document;

interface Extraction {
/**
* @return iterable<mixed, iterable<mixed, Coordinate>>
*/
public function __invoke(Document $document): iterable;
}
4 changes: 2 additions & 2 deletions packages/documentator/src/Markdown/Contracts/Mutation.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace LastDragon_ru\LaraASP\Documentator\Markdown\Contracts;

use LastDragon_ru\LaraASP\Documentator\Editor\Locations\Location;
use LastDragon_ru\LaraASP\Documentator\Editor\Coordinate;
use LastDragon_ru\LaraASP\Documentator\Markdown\Document;

interface Mutation {
/**
* @return iterable<array-key, array{Location, ?string}>
* @return iterable<mixed, array{iterable<mixed, Coordinate>, ?string}>
*/
public function __invoke(Document $document): iterable;
}
19 changes: 12 additions & 7 deletions packages/documentator/src/Markdown/Data/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,25 @@ final public function __construct(
* @return T
*/
public static function get(Node $node): mixed {
$data = $node->data->get(Package::Name.'.'.static::class, null);
$value = is_object($data) && is_a($data, static::class, true)
? $data->value
: static::default($node);
// Cached?
$data = $node->data->get(Package::Name.'.'.static::class, null);

if ($data === null && $value !== null) {
static::set($node, $value);
if (is_object($data) && is_a($data, static::class, true)) {
return $data->value;
}

// Default?
$value = static::default($node);

if ($value === null && is_a(static::class, Nullable::class, true)) {
return static::set($node, $value);
}

if ($value === null) {
throw new DataMissed($node, static::class);
}

return $value;
return static::set($node, $value);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/documentator/src/Markdown/Data/Nullable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\Documentator\Markdown\Data;

/**
* @internal
* @template T
*
* @extends Data<T|null>
*/
abstract readonly class Nullable extends Data {
// empty
}
139 changes: 13 additions & 126 deletions packages/documentator/src/Markdown/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,28 @@

namespace LastDragon_ru\LaraASP\Documentator\Markdown;

use Closure;
use LastDragon_ru\LaraASP\Core\Path\FilePath;
use LastDragon_ru\LaraASP\Documentator\Editor\Coordinate;
use LastDragon_ru\LaraASP\Documentator\Editor\Editor;
use LastDragon_ru\LaraASP\Documentator\Editor\Locations\Location;
use LastDragon_ru\LaraASP\Documentator\Markdown\Contracts\Extraction;
use LastDragon_ru\LaraASP\Documentator\Markdown\Contracts\Markdown;
use LastDragon_ru\LaraASP\Documentator\Markdown\Contracts\Mutation;
use LastDragon_ru\LaraASP\Documentator\Markdown\Data\Lines;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\Block\Document as DocumentNode;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Node;
use Override;
use Stringable;

use function array_key_first;
use function array_key_last;
use function array_values;
use function count;
use function implode;
use function is_int;
use function mb_ltrim;
use function mb_trim;
use function str_ends_with;
use function str_starts_with;

// todo(documentator): There is no way to convert AST back to Markdown yet
// https://github.com/thephpleague/commonmark/issues/419

class Document implements Stringable {
private ?Editor $editor = null;
private ?string $title = null;
private ?string $summary = null;
private ?Editor $editor = null;

public function __construct(
protected readonly Markdown $markdown,
Expand All @@ -50,63 +37,21 @@ public function isEmpty(): bool {
return !$this->node->hasChildren() && count($this->node->getReferenceMap()) === 0;
}

/**
* Returns the first `# Header` if present.
*/
public function getTitle(): ?string {
if ($this->title === null) {
$title = $this->getFirstNode(Heading::class, static fn ($n) => $n->getLevel() === 1);
$title = $this->getBlockText($title) ?? '';
$title = mb_trim(mb_ltrim("{$title}", '#'));
$this->title = $title;
}

return $this->title !== '' ? $this->title : null;
}

/**
* Returns the first paragraph if present.
*/
public function getSummary(): ?string {
if ($this->summary === null) {
$summary = $this->getSummaryNode();
$summary = $this->getBlockText($summary);
$summary = mb_trim("{$summary}");
$this->summary = $summary;
}

return $this->summary !== '' ? $this->summary : null;
}

/**
* Returns the rest of the document text after the summary.
*/
public function getBody(): ?string {
$summary = $this->getSummaryNode();
$start = $summary?->getEndLine();
$end = array_key_last($this->getLines());
$body = $start !== null && is_int($end)
? $this->getText(new Location($start + 1, $end))
: null;
$body = mb_trim((string) $body);
$body = $body !== '' ? $body : null;

return $body;
}

/**
* @param iterable<array-key, Coordinate> $location
*/
public function getText(iterable $location): string {
return (string) $this->getEditor()->extract([$location]);
}

public function mutate(Mutation ...$mutations): self {
public function mutate(Mutation|Extraction ...$mutations): self {
$document = clone $this;

foreach ($mutations as $mutation) {
$changes = $mutation($document);
$content = mb_trim((string) $document->getEditor()->mutate($changes))."\n";
$content = $mutation instanceof Extraction
? $document->getEditor()->extract($mutation($document))
: $document->getEditor()->mutate($mutation($document));
$content = mb_trim((string) $content);
$document = $this->markdown->parse($content, $document->path);
}

Expand All @@ -130,71 +75,13 @@ protected function getEditor(): Editor {
return $this->editor;
}

/**
* @template T of Node
*
* @param class-string<T> $class
* @param Closure(T): bool|null $filter
* @param Closure(Node): bool|null $skip
*
* @return ?T
*/
private function getFirstNode(string $class, ?Closure $filter = null, ?Closure $skip = null): ?Node {
$node = null;

foreach ($this->node->children() as $child) {
// Comment?
if (
$child instanceof HtmlBlock
&& str_starts_with($child->getLiteral(), '<!--')
&& str_ends_with($child->getLiteral(), '-->')
) {
continue;
}

// Skipped?
if ($skip !== null && $skip($child)) {
continue;
}

// Wanted?
if ($child instanceof $class) {
if ($filter === null || $filter($child)) {
$node = $child;
}

break;
}

// End
break;
}

return $node;
}

private function getBlockText(?AbstractBlock $node): ?string {
$startLine = $node?->getStartLine();
$endLine = $node?->getEndLine();
$location = $startLine !== null && $endLine !== null
? new Location($startLine, $endLine)
: null;
$text = $location !== null
? $this->getText($location)
: null;

return $text;
}

private function getSummaryNode(): ?Paragraph {
$skip = static fn ($node) => $node instanceof Heading && $node->getLevel() === 1;
$node = $this->getFirstNode(Paragraph::class, skip: $skip);

return $node;
}

#[Override]
public function __toString(): string {
return implode("\n", $this->getLines())."\n";
$lines = $this->getLines();
$string = $lines !== []
? implode("\n", $this->getLines())."\n"
: '';

return $string;
}
}
Loading

0 comments on commit ed14398

Please sign in to comment.