@@ -45,15 +45,18 @@ public function __construct()
4545 /**
4646 * Convert markdown to HTML with GitHub-style alert boxes, code block wrappers, and link transformation.
4747 */
48- public function toHtml (string $ markdown , ?string $ currentSection = null ): string
49- {
48+ public function toHtml (
49+ string $ markdown ,
50+ ?string $ currentSection = null ,
51+ ?string $ sourceFilePath = null ,
52+ ): string {
5053 $ markdown = $ this ->stripMarkdownArtifacts ($ markdown );
5154 $ html = $ this ->converter ->convert ($ markdown )->getContent ();
5255 $ html = $ this ->stripContentBeforeH1 ($ html );
5356 $ html = $ this ->convertGitHubAlerts ($ html );
5457 $ html = $ this ->wrapCodeBlocks ($ html );
5558
56- return $ this ->transformLinks ($ html , $ currentSection );
59+ return $ this ->transformLinks ($ html , $ currentSection, $ sourceFilePath );
5760 }
5861
5962 /**
@@ -104,7 +107,7 @@ private function stripMarkdownArtifacts(string $markdown): string
104107 * Convert GitHub-style alerts to styled callout boxes.
105108 *
106109 * Transforms blockquotes like:
107- * > [!INFO ]
110+ * > [!NOTE ]
108111 * > Content here
109112 *
110113 * Into styled alert boxes with icons using Blade component.
@@ -117,7 +120,7 @@ private function convertGitHubAlerts(string $html): string
117120 // ...optional additional block elements (e.g. <p>, <ul>, <pre>)...
118121 // </blockquote>
119122 return preg_replace_callback (
120- '/<blockquote>\s*<p>\s*\[!(INFO |IMPORTANT)\]\s*(.*?)<\/p>(.*?)<\/blockquote>/is ' ,
123+ '/<blockquote>\s*<p>\s*\[!(NOTE |IMPORTANT)\]\s*(.*?)<\/p>(.*?)<\/blockquote>/is ' ,
121124 function (array $ matches ): string {
122125 $ type = strtolower (trim ($ matches [1 ]));
123126 $ firstParagraph = trim ($ matches [2 ]);
@@ -178,11 +181,11 @@ function (array $matches): string {
178181 * Converts links like `section/file.md` or `/docs/section/page` to full
179182 * GitHub URLs pointing to the source repository.
180183 */
181- private function transformLinks (string $ html , ?string $ currentSection ): string
184+ private function transformLinks (string $ html , ?string $ currentSection, ? string $ sourceFilePath ): string
182185 {
183186 return preg_replace_callback (
184187 '/<a\s+href="([^"]+)"([^>]*)>/i ' ,
185- fn (array $ matches ): string => $ this ->transformLink ($ matches , $ currentSection ),
188+ fn (array $ matches ): string => $ this ->transformLink ($ matches , $ currentSection, $ sourceFilePath ),
186189 $ html
187190 ) ?? $ html ;
188191 }
@@ -195,7 +198,7 @@ private function transformLinks(string $html, ?string $currentSection): string
195198 *
196199 * @param array<int, string> $matches
197200 */
198- private function transformLink (array $ matches , ?string $ currentSection ): string
201+ private function transformLink (array $ matches , ?string $ currentSection, ? string $ sourceFilePath ): string
199202 {
200203 $ href = $ matches [1 ];
201204 $ attributes = $ matches [2 ];
@@ -212,6 +215,12 @@ private function transformLink(array $matches, ?string $currentSection): string
212215 return sprintf ('<a href="%s"%s> ' , $ internalUrl , $ attributes );
213216 }
214217
218+ $ relativeDocsUrl = $ this ->resolveRelativeDocsUrl ($ href , $ sourceFilePath );
219+
220+ if ($ relativeDocsUrl !== null ) {
221+ return sprintf ('<a href="%s"%s> ' , $ relativeDocsUrl , $ attributes );
222+ }
223+
215224 // All other links → GitHub with target="_blank"
216225 $ githubUrl = $ this ->resolveGitHubUrl ($ href , $ currentSection );
217226
@@ -222,6 +231,84 @@ private function transformLink(array $matches, ?string $currentSection): string
222231 );
223232 }
224233
234+ private function resolveRelativeDocsUrl (string $ href , ?string $ sourceFilePath ): ?string
235+ {
236+ if ($ sourceFilePath === null || str_starts_with ($ href , '/ ' )) {
237+ return null ;
238+ }
239+
240+ [$ path , $ fragment ] = $ this ->splitFragment ($ href );
241+
242+ if ($ path === '' || preg_match ('/^[a-z][a-z0-9+.-]*:/i ' , $ path ) === 1 ) {
243+ return null ;
244+ }
245+
246+ $ docsDirectory = config ('docs.path ' );
247+
248+ if (! is_string ($ docsDirectory ) || $ docsDirectory === '' ) {
249+ return null ;
250+ }
251+
252+ $ absoluteDocsDirectory = str_starts_with ($ docsDirectory , '/ ' )
253+ ? $ docsDirectory
254+ : base_path ($ docsDirectory );
255+
256+ $ resolvedDocsDirectory = realpath ($ absoluteDocsDirectory );
257+ $ resolvedSourcePath = realpath ($ sourceFilePath );
258+
259+ if ($ resolvedDocsDirectory === false || $ resolvedSourcePath === false ) {
260+ return null ;
261+ }
262+
263+ $ sourceDirectory = dirname ($ resolvedSourcePath );
264+ $ candidatePath = $ path ;
265+
266+ if (! str_ends_with ($ candidatePath , '.md ' )) {
267+ $ candidatePath .= '.md ' ;
268+ }
269+
270+ $ resolvedTargetPath = realpath ($ sourceDirectory .'/ ' .$ candidatePath );
271+
272+ if ($ resolvedTargetPath === false || ! str_ends_with ($ resolvedTargetPath , '.md ' )) {
273+ return null ;
274+ }
275+
276+ $ resolvedDocsParentReadme = realpath (dirname ($ resolvedDocsDirectory ).'/README.md ' );
277+
278+ if ($ resolvedDocsParentReadme !== false && $ resolvedTargetPath === $ resolvedDocsParentReadme ) {
279+ return '/ ' .$ fragment ;
280+ }
281+
282+ $ docsPrefix = rtrim ($ resolvedDocsDirectory , DIRECTORY_SEPARATOR ).DIRECTORY_SEPARATOR ;
283+
284+ if (! str_starts_with ($ resolvedTargetPath , $ docsPrefix )) {
285+ return null ;
286+ }
287+
288+ $ relativeTargetPath = substr ($ resolvedTargetPath , strlen ($ docsPrefix ));
289+ $ page = preg_replace ('/\.md$/ ' , '' , $ relativeTargetPath );
290+
291+ if ($ page === null || $ page === '' || preg_match ('/^[a-z0-9-]+$/ ' , $ page ) !== 1 ) {
292+ return null ;
293+ }
294+
295+ return "/docs/ {$ page }{$ fragment }" ;
296+ }
297+
298+ /**
299+ * @return array{0: string, 1: string}
300+ */
301+ private function splitFragment (string $ href ): array
302+ {
303+ if (! str_contains ($ href , '# ' )) {
304+ return [$ href , '' ];
305+ }
306+
307+ [$ path , $ fragment ] = explode ('# ' , $ href , 2 );
308+
309+ return [$ path , '# ' .$ fragment ];
310+ }
311+
225312 /**
226313 * Determine if a link points to a docs page by path prefix.
227314 */
0 commit comments