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
138 changes: 81 additions & 57 deletions .phpstorm.meta.php

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions _ide_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -24282,6 +24282,10 @@ public static function response($callback)
*/
class DocsViewer extends \Livewire\Component {
}
/**
*/
class CommandIndex extends \Livewire\Component {
}
}


Expand Down
2 changes: 1 addition & 1 deletion app/Services/DocumentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private function loadFromPath(string $filePath, string $documentIdentity, string
return null;
}

$html = $this->markdown->toHtml($content);
$html = $this->markdown->toHtml($content, $documentIdentity === 'readme' ? null : $titleFallback, $filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how currentSection is used in resolveGitHubUrl
echo "=== resolveGitHubUrl implementation ==="
rg -nA 20 'private function resolveGitHubUrl' app/Services/MarkdownService.php

echo -e "\n=== loadFromPath calls and titleFallback usage ==="
rg -nB 2 -A 5 'toHtml' app/Services/DocumentService.php | head -50

echo -e "\n=== MarkdownService toHtml signature ==="
rg -nA 5 'public function toHtml' app/Services/MarkdownService.php

Repository: loadinglucian/deployerphp.com

Length of output: 1633


🏁 Script executed:

#!/bin/bash
# Get complete resolveGitHubUrl function
echo "=== Complete resolveGitHubUrl implementation ==="
rg -nA 40 'private function resolveGitHubUrl' app/Services/MarkdownService.php

echo -e "\n=== How titleFallback is set in DocumentService ==="
rg -nB 10 'titleFallback' app/Services/DocumentService.php | grep -A 10 -B 10 'titleFallback ='

echo -e "\n=== toHtml method body to see how currentSection is used ==="
rg -nA 30 'public function toHtml' app/Services/MarkdownService.php

Repository: loadinglucian/deployerphp.com

Length of output: 2949


🏁 Script executed:

#!/bin/bash
# Check the docs directory structure
echo "=== Docs directory structure ==="
find docs -type f -name "*.md" 2>/dev/null | head -30

echo -e "\n=== How titleFallback is populated in DocumentService ==="
rg -nB 20 'toHtml' app/Services/DocumentService.php | grep -A 15 -B 15 'titleFallback'

echo -e "\n=== Load method to see how titleFallback comes from file path ==="
rg -nA 10 'public function load' app/Services/DocumentService.php | head -30

Repository: loadinglucian/deployerphp.com

Length of output: 1429


🏁 Script executed:

#!/bin/bash
# Check the loadFromPath signature and parameters
echo "=== DocumentService loadFromPath signature ==="
rg -nA 20 'private function loadFromPath' app/Services/DocumentService.php

echo -e "\n=== Check if docs directory exists and structure ==="
ls -la docs/ 2>/dev/null || echo "docs/ not found, checking other locations"
find . -maxdepth 2 -type d -name docs 2>/dev/null

echo -e "\n=== Check if there are test files or fixtures showing docs structure ==="
find . -name "*.md" -type f 2>/dev/null | head -20

echo -e "\n=== Check config for docs.github.dir ==="
rg -n 'docs\.github' config/

Repository: loadinglucian/deployerphp.com

Length of output: 1253


🏁 Script executed:

#!/bin/bash
# Check the test fixture docs structure
echo "=== Test fixture docs structure ==="
tree tests/Fixtures/docs/ 2>/dev/null || find tests/Fixtures/docs/ -type f

echo -e "\n=== Check test files that use these docs ==="
rg -l 'DocumentService' tests/ | head -5

echo -e "\n=== Look at a test file to understand expected behavior ==="
find tests/ -name "*Document*Test*" -type f -exec cat {} \; | head -100

echo -e "\n=== Check what config value is used for docs.github.dir ==="
rg -n "docs\.github\.dir" . 2>/dev/null

Repository: loadinglucian/deployerphp.com

Length of output: 3785


🏁 Script executed:

#!/bin/bash
# Check MarkdownServiceTest for GitHub URL tests
echo "=== MarkdownServiceTest GitHub URL tests ==="
rg -nA 20 'github' tests/Unit/MarkdownServiceTest.php -i

echo -e "\n=== Check if there are tests for resolveGitHubUrl specifically ==="
rg -nA 15 'resolveGitHubUrl\|currentSection' tests/Unit/MarkdownServiceTest.php

echo -e "\n=== Look at the actual test fixtures to see how links are expected to work ==="
cat tests/Fixtures/docs/docs/link-behavior.md

Repository: loadinglucian/deployerphp.com

Length of output: 2701


🏁 Script executed:

#!/bin/bash
# Check if there are tests for actual page loading with links
echo "=== Check DocumentServiceTest for link behavior ==="
cat tests/Unit/DocumentServiceTest.php

echo -e "\n=== Check if docs structure is hierarchical or flat in fixture ==="
rg -n "relative\|link" tests/Fixtures/docs/docs/*.md -i -A 2 -B 2

Repository: loadinglucian/deployerphp.com

Length of output: 3042


🏁 Script executed:

#!/bin/bash
# Read the actual fixture docs
echo "=== installation.md ==="
cat tests/Fixtures/docs/docs/installation.md

echo -e "\n=== documentation.md ==="
cat tests/Fixtures/docs/docs/documentation.md

echo -e "\n=== link-behavior.md ==="
cat tests/Fixtures/docs/docs/link-behavior.md

Repository: loadinglucian/deployerphp.com

Length of output: 752


🏁 Script executed:

#!/bin/bash
# Test what happens when documentation.md (flat structure) contains relative links
echo "=== Test the actual behavior with flat docs ==="
cat > /tmp/test_url.php << 'EOF'
<?php
// Simulate the resolveGitHubUrl logic
function testResolveGitHubUrl(string $href, ?string $currentSection): string {
    $docsDir = 'docs';
    $baseUrl = 'https://github.com/acme/repo/blob/main';
    
    // Handle .md links without slashes
    if (str_ends_with($href, '.md')) {
        if ($currentSection !== null && ! str_contains($href, '/')) {
            return "{$baseUrl}/{$docsDir}/{$currentSection}/{$href}";
        }
        return "{$baseUrl}/{$docsDir}/{$href}";
    }
    
    // Other relative paths
    if ($currentSection !== null && ! str_contains($href, '/')) {
        return "{$baseUrl}/{$docsDir}/{$currentSection}/{$href}.md";
    }
    return "{$baseUrl}/{$docsDir}/{$href}.md";
}

// Test case: documentation.md page with relative links to installation.md
echo "When documentation page contains '[Installation](installation.md)':\n";
echo "Generated URL: " . testResolveGitHubUrl('installation.md', 'documentation') . "\n";
echo "Expected URL:  https://github.com/acme/repo/blob/main/docs/installation.md\n\n";

// Test case: link with slash (should work correctly)
echo "When page contains '[Operations](operations/runbooks.md)':\n";
echo "Generated URL: " . testResolveGitHubUrl('operations/runbooks.md', 'documentation') . "\n";
echo "Expected URL:  https://github.com/acme/repo/blob/main/docs/operations/runbooks.md\n";
EOF

php /tmp/test_url.php

Repository: loadinglucian/deployerphp.com

Length of output: 576


Fix incorrect GitHub URL generation for relative links in flat docs structure.

Passing $titleFallback (the page slug) as $currentSection causes relative links without directory separators to generate incorrect GitHub URLs. For example, when documentation.md contains [Installation](installation.md), it generates https://github.com/repo/blob/main/docs/documentation/installation.md instead of https://github.com/repo/blob/main/docs/installation.md.

In a flat docs structure where pages like installation.md and documentation.md live directly in the docs directory, they should not have a "current section" prepended. The null value passed for README is correct; regular pages should use the same approach or $sourceFilePath should be used instead for GitHub URL resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/DocumentService.php` at line 84, The markdown-to-HTML call in
DocumentService (the line calling $this->markdown->toHtml in
DocumentService.php) is passing $titleFallback as the $currentSection which
causes relative links to be resolved as if they live under a subdirectory;
change the call to pass null for $currentSection (same as README) or pass the
actual $sourceFilePath used for GitHub resolution so links like
[Installation](installation.md) resolve to docs/installation.md instead of
docs/<page>/installation.md; update the invocation in the method that constructs
$html and ensure any tests or callers expecting the old behavior are adjusted
accordingly.

$headings = $this->headingExtractor->extract($html);
$title = $this->extractTitle($html, $titleFallback);

Expand Down
103 changes: 95 additions & 8 deletions app/Services/MarkdownService.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,18 @@ public function __construct()
/**
* Convert markdown to HTML with GitHub-style alert boxes, code block wrappers, and link transformation.
*/
public function toHtml(string $markdown, ?string $currentSection = null): string
{
public function toHtml(
string $markdown,
?string $currentSection = null,
?string $sourceFilePath = null,
): string {
$markdown = $this->stripMarkdownArtifacts($markdown);
$html = $this->converter->convert($markdown)->getContent();
$html = $this->stripContentBeforeH1($html);
$html = $this->convertGitHubAlerts($html);
$html = $this->wrapCodeBlocks($html);

return $this->transformLinks($html, $currentSection);
return $this->transformLinks($html, $currentSection, $sourceFilePath);
}

/**
Expand Down Expand Up @@ -104,7 +107,7 @@ private function stripMarkdownArtifacts(string $markdown): string
* Convert GitHub-style alerts to styled callout boxes.
*
* Transforms blockquotes like:
* > [!INFO]
* > [!NOTE]
* > Content here
*
* Into styled alert boxes with icons using Blade component.
Expand All @@ -117,7 +120,7 @@ private function convertGitHubAlerts(string $html): string
// ...optional additional block elements (e.g. <p>, <ul>, <pre>)...
// </blockquote>
return preg_replace_callback(
'/<blockquote>\s*<p>\s*\[!(INFO|IMPORTANT)\]\s*(.*?)<\/p>(.*?)<\/blockquote>/is',
'/<blockquote>\s*<p>\s*\[!(NOTE|IMPORTANT)\]\s*(.*?)<\/p>(.*?)<\/blockquote>/is',
function (array $matches): string {
$type = strtolower(trim($matches[1]));
$firstParagraph = trim($matches[2]);
Expand Down Expand Up @@ -178,11 +181,11 @@ function (array $matches): string {
* Converts links like `section/file.md` or `/docs/section/page` to full
* GitHub URLs pointing to the source repository.
*/
private function transformLinks(string $html, ?string $currentSection): string
private function transformLinks(string $html, ?string $currentSection, ?string $sourceFilePath): string
{
return preg_replace_callback(
'/<a\s+href="([^"]+)"([^>]*)>/i',
fn (array $matches): string => $this->transformLink($matches, $currentSection),
fn (array $matches): string => $this->transformLink($matches, $currentSection, $sourceFilePath),
$html
) ?? $html;
}
Expand All @@ -195,7 +198,7 @@ private function transformLinks(string $html, ?string $currentSection): string
*
* @param array<int, string> $matches
*/
private function transformLink(array $matches, ?string $currentSection): string
private function transformLink(array $matches, ?string $currentSection, ?string $sourceFilePath): string
{
$href = $matches[1];
$attributes = $matches[2];
Expand All @@ -212,6 +215,12 @@ private function transformLink(array $matches, ?string $currentSection): string
return sprintf('<a href="%s"%s>', $internalUrl, $attributes);
}

$relativeDocsUrl = $this->resolveRelativeDocsUrl($href, $sourceFilePath);

if ($relativeDocsUrl !== null) {
return sprintf('<a href="%s"%s>', $relativeDocsUrl, $attributes);
}

// All other links → GitHub with target="_blank"
$githubUrl = $this->resolveGitHubUrl($href, $currentSection);

Expand All @@ -222,6 +231,84 @@ private function transformLink(array $matches, ?string $currentSection): string
);
}

private function resolveRelativeDocsUrl(string $href, ?string $sourceFilePath): ?string
{
if ($sourceFilePath === null || str_starts_with($href, '/')) {
return null;
}

[$path, $fragment] = $this->splitFragment($href);

if ($path === '' || preg_match('/^[a-z][a-z0-9+.-]*:/i', $path) === 1) {
return null;
}

$docsDirectory = config('docs.path');

if (! is_string($docsDirectory) || $docsDirectory === '') {
return null;
}

$absoluteDocsDirectory = str_starts_with($docsDirectory, '/')
? $docsDirectory
: base_path($docsDirectory);

$resolvedDocsDirectory = realpath($absoluteDocsDirectory);
$resolvedSourcePath = realpath($sourceFilePath);

if ($resolvedDocsDirectory === false || $resolvedSourcePath === false) {
return null;
}

$sourceDirectory = dirname($resolvedSourcePath);
$candidatePath = $path;

if (! str_ends_with($candidatePath, '.md')) {
$candidatePath .= '.md';
}

$resolvedTargetPath = realpath($sourceDirectory.'/'.$candidatePath);

if ($resolvedTargetPath === false || ! str_ends_with($resolvedTargetPath, '.md')) {
return null;
}

$resolvedDocsParentReadme = realpath(dirname($resolvedDocsDirectory).'/README.md');

if ($resolvedDocsParentReadme !== false && $resolvedTargetPath === $resolvedDocsParentReadme) {
return '/'.$fragment;
}

$docsPrefix = rtrim($resolvedDocsDirectory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;

if (! str_starts_with($resolvedTargetPath, $docsPrefix)) {
return null;
}

$relativeTargetPath = substr($resolvedTargetPath, strlen($docsPrefix));
$page = preg_replace('/\.md$/', '', $relativeTargetPath);

if ($page === null || $page === '' || preg_match('/^[a-z0-9-]+$/', $page) !== 1) {
return null;
}

return "/docs/{$page}{$fragment}";
}

/**
* @return array{0: string, 1: string}
*/
private function splitFragment(string $href): array
{
if (! str_contains($href, '#')) {
return [$href, ''];
}

[$path, $fragment] = explode('#', $href, 2);

return [$path, '#'.$fragment];
}

/**
* Determine if a link points to a docs page by path prefix.
*/
Expand Down
20 changes: 10 additions & 10 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions tests/Feature/DocsSidebarNavigationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
->assertSeeText('Requirements');
});

it('converts relative markdown docs links to internal routes', function (): void {
app()->forgetInstance(DocsPathService::class);
app()->forgetInstance(TocParserService::class);
app()->forgetInstance(DocumentService::class);

$response = $this->get(route('docs.show', ['page' => 'documentation']));

$response->assertOk()
->assertSee('href="'.route('home').'"', false)
->assertSee('href="'.route('docs.show', ['page' => 'installation']).'"', false)
->assertSee('href="'.route('docs.show', ['page' => 'link-behavior']).'"', false)
->assertSee('href="https://github.com/loadinglucian/deployer-php/blob/main/docs/operations/runbooks.md" target="_blank" rel="noopener noreferrer"', false);
});

it('redirects missing docs pages to the docs home', function (): void {
app()->forgetInstance(DocsPathService::class);
app()->forgetInstance(TocParserService::class);
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/docs/docs/documentation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<a name="guides"></a>
## Guides
- [Introduction](README.md)
- [Introduction](../README.md)
- [Installation](installation.md)
- [Link Behavior](link-behavior.md)

Expand Down
17 changes: 17 additions & 0 deletions tests/Unit/MarkdownServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@
->toContain('<li>Second item</li>');
});

it('renders note alerts with block content', function (): void {
$markdown = <<<'MARKDOWN'
> [!NOTE]
> First note callout.
>
> - First item
> - Second item
MARKDOWN;

$html = app(MarkdownService::class)->toHtml($markdown);

expect(substr_count($html, 'dark:border-l-cyan-400'))->toBe(1)
->and($html)
->toContain('<li>First item</li>')
->toContain('<li>Second item</li>');
});

it('keeps empty github alert markers unchanged when there is no content', function (): void {
$markdown = <<<'MARKDOWN'
> [!IMPORTANT]
Expand Down