feat(markdown): render inline {@link Target} references as links#181
Conversation
📝 WalkthroughWalkthroughThis PR adds TSDoc/JSDoc ChangesInline
Sequence DiagramsequenceDiagram
participant App as App.fetchData()
participant Registry as markdown-types
participant MarkdownTsx as Markdown.renderMarkdown()
participant Pipeline as markdownToHtml()
participant Normalize as normalizeInlineLinkUrls()
participant Resolver as createLinkResolver()
participant InlineLinks as inlineLinks()
participant TypeLinks as typeLinks()
participant HTML as HTML Output
App->>Registry: setTypes(typeNames)
App->>Registry: setComponents(componentTags)
MarkdownTsx->>MarkdownTsx: types = getTypes()
MarkdownTsx->>MarkdownTsx: components = getComponents()
MarkdownTsx->>Pipeline: markdownToHtml(text, types, components)
Pipeline->>Normalize: normalizeInlineLinkUrls(text)
Normalize->>Pipeline: markdown with [label](url) links
Pipeline->>Resolver: createLinkResolver(types, components)
Resolver->>Pipeline: resolver: (target) => url | null
Pipeline->>InlineLinks: inlineLinks({resolve})
InlineLinks->>InlineLinks: walk mdast text nodes
InlineLinks->>Resolver: resolve(target)
Resolver->>InlineLinks: `#/type/X` or `#/component/Y/`
InlineLinks->>Pipeline: mdast link nodes
Pipeline->>TypeLinks: typeLinks() transformer
TypeLinks->>TypeLinks: collect skippable code (inside pre or anchor)
TypeLinks->>Pipeline: avoid wrapping types in links
Pipeline->>HTML: final HTML with resolved links
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
When an inline `<code>` element already sits inside an `<a>` (for example, from a markdown link like `[label `Foo`](url)`), the typeLinks pass would wrap the type token in another anchor and produce a nested `<a>`, which is invalid HTML and confuses navigation. Generalises the existing `<pre><code>` skip-set into a more general "skippable code" set that also captures `<code>` inside any anchor ancestor, and leaves those code elements untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JSDoc/TSDoc `{@link Target}` references in component descriptions
previously surfaced in the generated docs as literal text. Adds a remark
plugin that rewrites them into mdast link nodes during the markdown
pipeline, supporting the standard syntactic forms:
{@link Target}
{@link Target Display text}
{@link Target | Display text}
{@link https://example.com | Display text}
Targets are resolved against the project's known types and component
tags (a new component registry mirrors the existing type registry).
Bare-identifier references — those without an explicit display label —
are wrapped in `<code>` so they visually match how type names render
elsewhere in the docs. Unresolved targets still render as inline code
rather than dangling syntax, and absolute URLs link directly.
References inside inline code (``{@link Foo}``) and fenced code blocks
are intentionally left untouched so the syntax itself can be documented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dedicated `kompendium-example-inline-links` example component
attached to `kompendium-markdown` so the rendered docs page shows each
inline `{@link …}` syntactic form alongside its source. The integration
test renders the example against the actual built `kompendium.json`
registry to keep the showcase honest as the docs build evolves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4e5284e to
8d14c39
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/kompendium/markdown-inline-links.ts`:
- Around line 65-70: The normalizeInlineLinkUrls() function applies URL pattern
matching across the entire text without distinguishing between prose and code
content, which breaks inline code and fenced code blocks. Refactor the function
to first parse and identify code spans (delimited by backticks) and fenced code
blocks (delimited by triple backticks), then apply the URL replacement only to
non-code prose segments while preserving all code content as literal text.
- Around line 44-52: The splitLabel() function incorrectly identifies the first
occurrence of a pipe character anywhere in the rest string as a label separator,
which can truncate valid labels containing pipes. Instead of using indexOf() to
find any pipe, modify the logic to only treat a pipe as a separator when it
appears at the start of the label section. Check if the rest string begins with
a pipe character or restructure the parsing logic to ensure pipes that appear
later in the content are not mistaken for the separator.
- Around line 101-111: The guard condition in the visit callback for text nodes
only checks if the immediate parent is a link type, but it fails when text nodes
are nested deeper within links through intermediate formatting elements like
strong or emphasis. Modify the logic to pre-collect all text node descendants of
link nodes before processing, then check if the current text node belongs to
that collection instead of only checking parent.type === 'link'. This ensures
all text nodes under any link in the tree hierarchy are skipped, regardless of
nesting depth through formatting nodes.
In `@src/kompendium/test/markdown-inline-links.spec.ts`:
- Around line 152-170: The performance assertions in the two test cases
('returns promptly without rewriting (mdast plugin)' and 'returns promptly
without rewriting (URL pre-pass)') use hardcoded 100ms thresholds with
Date.now() comparisons, which are brittle and prone to flaking in noisy CI
environments. Replace these tight fixed-time assertions with either
significantly relaxed thresholds (e.g., 500ms or 1000ms) to reduce false
failures while still catching true complexity regressions, or implement a
relative-growth assertion pattern that compares performance against a baseline
rather than an absolute time value.
In `@src/kompendium/test/markdown.spec.ts`:
- Around line 565-586: The two test functions "rewrites a URL reference even
inside inline code (known gap)" and "rewrites a URL reference even inside fenced
code (known gap)" have assertions that expect `{`@link`}` URL references to be
rewritten into markdown links even when inside code blocks. This conflicts with
the stated contract that references inside code should remain unchanged. Either
update both test assertions to expect the original `{`@link`}` syntax to be
preserved literally in the code blocks (and fix the underlying pipeline to
achieve this), or update the contract documentation to explicitly state that URL
rewriting inside code blocks is intentional behavior. Choose one consistent
approach and align the tests, implementation, and documentation accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b5c033fa-9951-4180-aa33-3db802417a65
📒 Files selected for processing (11)
src/components/app/app.tsxsrc/components/markdown/examples/inline-links-example.tssrc/components/markdown/examples/inline-links.tsxsrc/components/markdown/markdown-types.tssrc/components/markdown/markdown.tsxsrc/kompendium/markdown-inline-links.tssrc/kompendium/markdown-typelinks.tssrc/kompendium/markdown.tssrc/kompendium/test/markdown-example-integration.spec.tssrc/kompendium/test/markdown-inline-links.spec.tssrc/kompendium/test/markdown.spec.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- src/components/app/app.tsx
- src/kompendium/markdown.ts
- src/components/markdown/examples/inline-links-example.ts
- src/components/markdown/examples/inline-links.tsx
- src/components/markdown/markdown-types.ts
- src/kompendium/markdown-typelinks.ts
- src/components/markdown/markdown.tsx
- src/kompendium/test/markdown-example-integration.spec.ts
| const pipeIndex = rest.indexOf('|'); | ||
| if (pipeIndex !== -1) { | ||
| const pipeLabel = rest.slice(pipeIndex + 1).trim(); | ||
| if (pipeLabel.length > 0) { | ||
| return { label: pipeLabel, explicit: true }; | ||
| } | ||
|
|
||
| return { label: null, explicit: false }; | ||
| } |
There was a problem hiding this comment.
Only treat | as a separator when it starts the label section.
splitLabel() currently uses the first | anywhere in rest, which can truncate valid space-form labels containing pipes.
Suggested fix
function splitLabel(rest: string): SplitLabel {
- const pipeIndex = rest.indexOf('|');
- if (pipeIndex !== -1) {
- const pipeLabel = rest.slice(pipeIndex + 1).trim();
+ const trimmedStart = rest.trimStart();
+ if (trimmedStart.startsWith('|')) {
+ const pipeLabel = trimmedStart.slice(1).trim();
if (pipeLabel.length > 0) {
return { label: pipeLabel, explicit: true };
}
return { label: null, explicit: false };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const pipeIndex = rest.indexOf('|'); | |
| if (pipeIndex !== -1) { | |
| const pipeLabel = rest.slice(pipeIndex + 1).trim(); | |
| if (pipeLabel.length > 0) { | |
| return { label: pipeLabel, explicit: true }; | |
| } | |
| return { label: null, explicit: false }; | |
| } | |
| const trimmedStart = rest.trimStart(); | |
| if (trimmedStart.startsWith('|')) { | |
| const pipeLabel = trimmedStart.slice(1).trim(); | |
| if (pipeLabel.length > 0) { | |
| return { label: pipeLabel, explicit: true }; | |
| } | |
| return { label: null, explicit: false }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kompendium/markdown-inline-links.ts` around lines 44 - 52, The
splitLabel() function incorrectly identifies the first occurrence of a pipe
character anywhere in the rest string as a label separator, which can truncate
valid labels containing pipes. Instead of using indexOf() to find any pipe,
modify the logic to only treat a pipe as a separator when it appears at the
start of the label section. Check if the rest string begins with a pipe
character or restructure the parsing logic to ensure pipes that appear later in
the content are not mistaken for the separator.
| export function normalizeInlineLinkUrls(text: string): string { | ||
| return text.replace(URL_LINK_PATTERN, (_match, url, rest) => { | ||
| const { label } = splitLabel(rest); | ||
|
|
||
| return `[${(label ?? url).trim()}](${url})`; | ||
| }); |
There was a problem hiding this comment.
Prevent URL {@link ...} normalization inside code spans/fenced blocks.
normalizeInlineLinkUrls() is a raw string replace, so it also rewrites code content. This breaks the contract that inline/fenced code should remain literal.
Suggested fix (apply URL replacement only to prose segments)
export function normalizeInlineLinkUrls(text: string): string {
- return text.replace(URL_LINK_PATTERN, (_match, url, rest) => {
- const { label } = splitLabel(rest);
-
- return `[${(label ?? url).trim()}](${url})`;
- });
+ return splitMarkdownIntoCodeAndProse(text)
+ .map((segment) => {
+ if (segment.kind === 'code') {
+ return segment.value;
+ }
+
+ return segment.value.replace(URL_LINK_PATTERN, (_match, url, rest) => {
+ const { label } = splitLabel(rest);
+ return `[${(label ?? url).trim()}](${url})`;
+ });
+ })
+ .join('');
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kompendium/markdown-inline-links.ts` around lines 65 - 70, The
normalizeInlineLinkUrls() function applies URL pattern matching across the
entire text without distinguishing between prose and code content, which breaks
inline code and fenced code blocks. Refactor the function to first parse and
identify code spans (delimited by backticks) and fenced code blocks (delimited
by triple backticks), then apply the URL replacement only to non-code prose
segments while preserving all code content as literal text.
| visit( | ||
| tree, | ||
| 'text', | ||
| (node: MdastText, index: number | null, parent: Parent | null) => { | ||
| if (!parent || index === null) { | ||
| return; | ||
| } | ||
|
|
||
| if (parent.type === 'link') { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Skip all text nodes under existing links, not only direct link children.
Current guard only checks parent.type === 'link'. If text is nested inside formatting within a link (for example, link -> strong -> text), it will still be transformed and can create nested anchors.
Suggested fix (pre-collect text descendants of existing links)
return (tree: Node) => {
+ const textUnderExistingLinks = new Set<Node>();
+ visit(tree, 'link', (linkNode: Parent) => {
+ visit(linkNode, 'text', (textNode: Node) => {
+ textUnderExistingLinks.add(textNode);
+ });
+ });
+
visit(
tree,
'text',
(node: MdastText, index: number | null, parent: Parent | null) => {
if (!parent || index === null) {
return;
}
- if (parent.type === 'link') {
+ if (textUnderExistingLinks.has(node)) {
return;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kompendium/markdown-inline-links.ts` around lines 101 - 111, The guard
condition in the visit callback for text nodes only checks if the immediate
parent is a link type, but it fails when text nodes are nested deeper within
links through intermediate formatting elements like strong or emphasis. Modify
the logic to pre-collect all text node descendants of link nodes before
processing, then check if the current text node belongs to that collection
instead of only checking parent.type === 'link'. This ensures all text nodes
under any link in the tree hierarchy are skipped, regardless of nesting depth
through formatting nodes.
| it('returns promptly without rewriting (mdast plugin)', () => { | ||
| const resolve: LinkResolver = () => '#/type/X'; | ||
| const start = Date.now(); | ||
| const result = run(tree(paragraph(text(unterminated))), resolve); | ||
| expect(Date.now() - start).toBeLessThan(100); | ||
| // No `}`, so nothing matches: the text node is left as-is. | ||
| expect(result.children[0]).toEqual(paragraph(text(unterminated))); | ||
| }); | ||
|
|
||
| it('returns promptly without rewriting (URL pre-pass)', () => { | ||
| const start = Date.now(); | ||
| const result = normalizeInlineLinkUrls( | ||
| `{@link https://example.com${' '.repeat(50000)}`, | ||
| ); | ||
| expect(Date.now() - start).toBeLessThan(100); | ||
| expect(result).toEqual( | ||
| `{@link https://example.com${' '.repeat(50000)}`, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Avoid brittle fixed-time assertions for the short perf checks.
The Date.now() + toBeLessThan(100) guards are likely to flake under noisy CI load even without regressions. Keep the regression intent, but relax these short thresholds (or use a relative-growth assertion) so failures better indicate true complexity regressions.
Also applies to: 179-198
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kompendium/test/markdown-inline-links.spec.ts` around lines 152 - 170,
The performance assertions in the two test cases ('returns promptly without
rewriting (mdast plugin)' and 'returns promptly without rewriting (URL
pre-pass)') use hardcoded 100ms thresholds with Date.now() comparisons, which
are brittle and prone to flaking in noisy CI environments. Replace these tight
fixed-time assertions with either significantly relaxed thresholds (e.g., 500ms
or 1000ms) to reduce false failures while still catching true complexity
regressions, or implement a relative-growth assertion pattern that compares
performance against a baseline rather than an absolute time value.
| // Known limitation: the URL pre-pass (`normalizeInlineLinkUrls`) runs over | ||
| // raw text before parsing, so unlike the non-URL mdast path it cannot tell | ||
| // a code span from prose. A URL `{@link}` inside inline or fenced code is | ||
| // therefore rewritten to markdown link source rather than passed through | ||
| // verbatim. Documented here as intended (if imperfect) behaviour; non-URL | ||
| // references in code are correctly passed through (see the tests above). | ||
| it('rewrites a URL reference even inside inline code (known gap)', async () => { | ||
| const md = 'Use `{@link https://example.com}` literally.'; | ||
| const result = await markdownToHtml(md); | ||
| const html = result.toString(); | ||
| expect(html).toContain('<code>'); | ||
| expect(html).toContain('[https://example.com](https://example.com)'); | ||
| expect(html).not.toContain('{@link'); | ||
| }); | ||
|
|
||
| it('rewrites a URL reference even inside fenced code (known gap)', async () => { | ||
| const md = '```\n{@link https://example.com}\n```'; | ||
| const result = await markdownToHtml(md); | ||
| const html = result.toString(); | ||
| expect(html).toContain('[https://example.com](https://example.com)'); | ||
| expect(html).not.toContain('{@link'); | ||
| }); |
There was a problem hiding this comment.
These assertions lock in behavior that conflicts with the feature contract.
This block treats URL {@link ...} rewriting inside inline/fenced code as expected, but the stated contract for this PR is that references inside code should stay unchanged. Please align one source of truth: either flip these tests to preserve literal code content and fix the pipeline, or explicitly revise the contract/docs to make this behavior official.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/kompendium/test/markdown.spec.ts` around lines 565 - 586, The two test
functions "rewrites a URL reference even inside inline code (known gap)" and
"rewrites a URL reference even inside fenced code (known gap)" have assertions
that expect `{`@link`}` URL references to be rewritten into markdown links even
when inside code blocks. This conflicts with the stated contract that references
inside code should remain unchanged. Either update both test assertions to
expect the original `{`@link`}` syntax to be preserved literally in the code
blocks (and fix the underlying pipeline to achieve this), or update the contract
documentation to explicitly state that URL rewriting inside code blocks is
intentional behavior. Choose one consistent approach and align the tests,
implementation, and documentation accordingly.
|
🎉 This PR is included in version 1.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
JSDoc/TSDoc
{@link Target}references inside component descriptionspreviously surfaced in the generated docs as literal text — readers saw
{@link Rule}verbatim with nothing clickable. This PR teaches themarkdown pipeline to rewrite those references into proper links, the
same way IDEs interpret them for cmd-click navigation.
The change is split into three atomic commits:
fix(markdown): skip type-linking inside anchor ancestors— Aprerequisite bug fix in
typeLinks. Without it, a<code>elementinside an existing
<a>would get its type token wrapped in asecond
<a>, producing invalid nested anchors. Generalises theexisting
<pre><code>skip-set into a broader "skippable code" setthat also captures code inside any anchor ancestor.
feat(markdown): render inline {@link Target} references as links— Adds a remark plugin that walks mdast text nodes for{@link …}syntax and emits link nodes. Supports the three TSDocforms (
{@link Target},{@link Target Display},{@link Target | Display}) plus absolute URLs. Targets are resolvedagainst the project's known types and component tags via a new
component registry that mirrors the existing type registry. Bare
identifiers are wrapped in
<code>so they match how type namesrender elsewhere; unresolved targets still render as inline code
instead of bare prose; references inside inline and fenced code are
intentionally left untouched.
docs(markdown): add example demonstrating inline @link references— Addskompendium-example-inline-linksso therendered
kompendium-markdownpage shows each syntactic formalongside its source. An integration test pins the rendered HTML
against the actual built
kompendium.jsonregistry to keep theshowcase honest.
Behaviour matrix
{@link MenuItem}(known type)<a href="#/type/MenuItem"><code>MenuItem</code></a>{@link kompendium-markdown}(known component)<a href="#/component/kompendium-markdown/"><code>kompendium-markdown</code></a>{@link MenuItem | the menu item}(free-form label)<a href="#/type/MenuItem">the menu item</a>{@link https://example.com | docs}(absolute URL)<a href="https://example.com">docs</a>{@link Unknown}(unresolved bare identifier)<code>Unknown</code>`{@link Foo}`(inside inline code){@link Foo}inside```fenced blockTest plan
(
markdown-inline-links.spec.ts)forms, inline-code passthrough, fenced-code passthrough, and URL
targets (
markdown.spec.ts)Kompendium's own built registry (
markdown-example-integration.spec.ts)npm run buildcleannpm run lint:srccleannpm test— 115 passing, 5 pre-existing skipsnpm start→ navigate tokompendium-markdown→visually verify the new "Inline @link references" example
Notes
contain no
{@link …}references are unaffected.(nested
<a>is invalid HTML) and is a hard prerequisite for thefeature: without it,
{@link KnownType}would produce nested anchors.🤖 Generated with Claude Code
Summary by CodeRabbit
{@link...}syntax support for clickable documentation links to components, types, and external URLs.