1818 */
1919import type MarkdownIt from 'markdown-it'
2020import { getLocaleByPath , type LocaleConfig } from './locales'
21+ import { getEntry } from './func-registry'
2122
2223interface Param {
2324 name : string
@@ -59,12 +60,68 @@ export function funcBlockPlugin(md: MarkdownIt) {
5960 return true
6061 } )
6162
62- md . renderer . rules [ 'func_block' ] = ( tokens , idx ) => {
63- return renderFuncBlock ( md , tokens [ idx ] . content , tokens [ idx ] . meta ?. locale )
63+ md . renderer . rules [ 'func_block' ] = ( tokens , idx , options , env ) => {
64+ return renderFuncBlock ( md , tokens [ idx ] . content , tokens [ idx ] . meta ?. locale , env )
65+ }
66+
67+ // Inline rule: parse <func>..FQN..</func> references
68+ md . inline . ruler . before ( 'html_inline' , 'func_ref' , ( state , silent ) => {
69+ if ( state . src . slice ( state . pos , state . pos + 6 ) !== '<func>' ) return false
70+
71+ const closeIdx = state . src . indexOf ( '</func>' , state . pos + 6 )
72+ if ( closeIdx === - 1 ) return false
73+
74+ if ( silent ) return true
75+
76+ const token = state . push ( 'func_ref' , '' , 0 )
77+ token . content = state . src . slice ( state . pos + 6 , closeIdx ) . trim ( )
78+
79+ const relativePath = state . env ?. relativePath || ''
80+ token . meta = { locale : getLocaleByPath ( '/' + relativePath ) }
81+
82+ state . pos = closeIdx + 7
83+ return true
84+ } )
85+
86+ // Renderer for <func> inline references
87+ md . renderer . rules [ 'func_ref' ] = ( tokens , idx ) => {
88+ const token = tokens [ idx ]
89+ const rawFqn = token . content
90+ const locale = token . meta ?. locale
91+
92+ // Build display name: strip namespace, keep ()
93+ const displayFqn = stripNamespace ( rawFqn ) . display
94+
95+ // Look up in registry
96+ const entry = getEntry ( locale ?. code ?? 'en' , rawFqn )
97+
98+ if ( entry ) {
99+ const sigHtml = highlightSignature ( md , entry . signature )
100+ // Highlight inline display using the full signature, then extract the short portion
101+ const displayHtml = highlightFuncRef ( md , displayFqn , entry . signature )
102+ const shortHtml = entry . short ? md . renderInline ( entry . short ) : ''
103+
104+ const tooltip = `<span class="func-ref-tooltip">`
105+ + `<code class="func-ref-tooltip-sig vp-code">${ sigHtml } </code>`
106+ + ( shortHtml ? `<span class="func-ref-tooltip-short">${ shortHtml } </span>` : '' )
107+ + `</span>`
108+
109+ // Link only if signature has a navigable anchor (h > 0)
110+ if ( entry . hasAnchor ) {
111+ const href = entry . pagePath + '#' + entry . slug
112+ return `<a href="${ href } " class="func-ref vp-code">${ displayHtml } ${ tooltip } </a>`
113+ }
114+
115+ // Tooltip without link
116+ return `<span class="func-ref vp-code">${ displayHtml } ${ tooltip } </span>`
117+ }
118+
119+ // No match in registry — render as plain inline code
120+ return `<code>${ escapeHtml ( displayFqn ) } </code>`
64121 }
65122}
66123
67- function renderFuncBlock ( md : MarkdownIt , raw : string , locale ?: LocaleConfig ) : string {
124+ function renderFuncBlock ( md : MarkdownIt , raw : string , locale ?: LocaleConfig , env ?: any ) : string {
68125 // Parse name attribute (the full signature)
69126 const nameMatch = raw . match ( / < s i g n a t u r e \s + [ ^ > ] * n a m e = " ( [ ^ " ] * ) " / )
70127 if ( ! nameMatch ) return ''
@@ -122,12 +179,12 @@ function renderFuncBlock(md: MarkdownIt, raw: string, locale?: LocaleConfig): st
122179
123180 // Build HTML output
124181 const sigHtml = highlightSignature ( md , display )
125- const descHtml = description ? md . render ( description ) : ''
182+ const descHtml = description ? md . render ( description , env ) : ''
126183
127184 // Compact mode: everything inline, no section headers
128185 if ( compact ) {
129186 const shortHtml = short ? md . renderInline ( short ) : ''
130- const compactDescHtml = description ? md . render ( description ) : ''
187+ const compactDescHtml = description ? md . render ( description , env ) : ''
131188
132189 let html = '<div class="func-compact">'
133190
@@ -146,7 +203,7 @@ function renderFuncBlock(md: MarkdownIt, raw: string, locale?: LocaleConfig): st
146203 }
147204
148205 for ( const ex of examples ) {
149- html += `<div class="func-compact-example">${ md . render ( ex ) } </div>`
206+ html += `<div class="func-compact-example">${ md . render ( ex , env ) } </div>`
150207 }
151208
152209 html += '</div>\n'
@@ -189,7 +246,7 @@ function renderFuncBlock(md: MarkdownIt, raw: string, locale?: LocaleConfig): st
189246 html += ' <div class="func-section">\n'
190247 html += ` <p class="func-section-title">${ escapeHtml ( examplesLabel ) } </p>\n`
191248 for ( const ex of examples ) {
192- html += ` <div class="func-example">${ md . render ( ex ) } </div>\n`
249+ html += ` <div class="func-example">${ md . render ( ex , env ) } </div>\n`
193250 }
194251 html += ' </div>\n'
195252 }
@@ -243,6 +300,15 @@ function extractShortName(display: string): string {
243300 return funcMatch ? funcMatch [ 1 ] : display
244301}
245302
303+ /**
304+ * Highlights a short func reference (e.g. `Assert::blank()`) by wrapping it
305+ * as a PHP static call expression so Shiki can tokenize it properly.
306+ */
307+ function highlightFuncRef ( md : MarkdownIt , displayFqn : string , _fullSignature : string ) : string {
308+ // displayFqn is e.g. "Assert::blank()" — valid PHP as a static method call
309+ return highlightSignature ( md , displayFqn ) || escapeHtml ( displayFqn )
310+ }
311+
246312/**
247313 * Highlights a PHP signature string using Shiki via markdown-it's highlight option.
248314 */
0 commit comments