Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown extraction #220

Merged
merged 14 commits into from
Jan 23, 2025
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
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
Loading