Skip to content

feat(markdown): render inline {@link Target} references as links#5

Merged
adrianschmidt merged 3 commits into
limefrom
feat/inline-link-references
Jun 17, 2026
Merged

feat(markdown): render inline {@link Target} references as links#5
adrianschmidt merged 3 commits into
limefrom
feat/inline-link-references

Conversation

@adrianschmidt

Copy link
Copy Markdown

Fork-side pair of upstream PR jgroth#181 by @Kiarokh. Same
main-based branch, so the identical change lands on both sides and the daily
upstream sync merges it as a no-op once upstream accepts.

Note

Per FORK.md, this PR will show as "out-of-date with the base branch"
permanently and by design (lime carries the fork delta). Do not use
the Update branch button.

Summary

JSDoc/TSDoc {@link Target} references inside component descriptions
previously surfaced in the generated docs as literal text — readers saw
{@link Rule} verbatim with nothing clickable. This teaches the markdown
pipeline to rewrite those references into proper links, the same way IDEs
interpret them for cmd-click navigation.

Three atomic commits:

  1. fix(markdown): skip type-linking inside anchor ancestors — prerequisite
    bug fix: a <code> inside an existing <a> would otherwise get wrapped in a
    second <a> (invalid nested anchors).
  2. feat(markdown): render inline {@link Target} references as links — a
    remark plugin that rewrites {@link …} (the three TSDoc forms + absolute
    URLs) into link nodes, resolving targets against known types and component
    tags. Unresolved targets render as inline code; references inside inline/
    fenced code are left untouched.
  3. docs(markdown): add example demonstrating inline @link references — a
    kompendium-example-inline-links showcase plus an integration test pinning
    the rendered HTML against the built kompendium.json registry.

Verification (on the lime merge result)

npm run lint:src, npm run build, and npm test (119 passing / 6 skipped)
all green against lime + this branch.


Authored by @Kiarokh — see the upstream PR for full description and the
behaviour matrix.

@adrianschmidt

Copy link
Copy Markdown
Author

Consolidated PR Review

6-agent review (+ 7th Lime-platform dimension). Reviewed feat/inline-link-references @ 4e5284e against target branch lime. The ReDoS finding below was verified independently (measurements reproduced).

PR Summary

Teaches the markdown pipeline to rewrite inline JSDoc/TSDoc {@link Target} references into clickable links, resolving targets against the project's known types and a new component registry. Three atomic commits: a prerequisite typeLinks fix (skip type-linking inside anchor ancestors to avoid invalid nested <a>), the inlineLinks remark plugin + URL pre-pass, and an example component with an integration test. 11 files, +587/−20.

Merge Readiness — BLOCKED ❌

The PR is a clean, well-tested, almost entirely additive feature and is better than lime in every dimension except one: it introduces a new catastrophic-backtracking (ReDoS) cliff in the two {@link} regexes that runs synchronously on the browser render thread. That is a new performance cliff that didn't exist on lime, so it blocks merge under the "no new regression" bar — though it's narrow in trigger and the fix is a one-liner.

  • Blockers:
    1. [High — Performance] Catastrophic regex backtracking in URL_LINK_PATTERN / INLINE_LINK_PATTERN (src/kompendium/markdown-inline-links.ts:6-7, 41-42). An unterminated {@link X followed by a long whitespace run blows up super-linearly on the main thread (independently measured: 1 000 spaces ≈ 190 ms, 2 000 ≈ 1.5 s, 3 000 ≈ 5 s; terminated/normal input is ~0 ms). Build-time, developer-authored input makes this an accidental-DoS footgun rather than an external attack surface — but it ships into kompendium.json and would freeze the rendered page for every reader of an affected doc. Cheap bounded fix.
  • Non-blocking but worth addressing: A handful of [Low] items (regex duplication, silent describe.skip in the integration test, a redundant resolver branch). See Top Recommendations — none gate merge.

1. Backward Compatibility — SAFE ✅

Purely additive at every public surface. The sole signature change is a new optional parameter; all renames are module-private; the one behavioral delta to existing rendering fixes a latent invalid-HTML bug.

  • [Low] Behavior change for downstream docs containing literal {@link …} (src/kompendium/markdown-inline-links.ts). Consumer component descriptions that previously surfaced {@link X} as literal text now render as links / inline code. This is the PR's stated, intended feature; the unresolved-target fallback (inline code or plain prose, never a dangling link) keeps it safe. No such content exists in this repo.
  • Positives: New components param is optional and safely defaulted; the sole production call site (markdown.tsx:57) is updated; the skip-set change strictly fixes a pre-existing nested-anchor case; app.tsx:87 guards missing docs.components with ?./?? [].

2. Code Quality — GOOD ✅

Cohesive, well-decomposed new code with a thorough test matrix that mirrors the description's behaviour table. Both regex-state concerns were checked and come out safe; findings are minor.

  • [Low] Near-duplicate regexes (markdown-inline-links.ts:6, 41): URL_LINK_PATTERN and INLINE_LINK_PATTERN differ only in the target capture; the label-alternation tail is duplicated verbatim and must stay in lockstep. A shared fragment would prevent drift. (Tightly related to the blocker fix — fix both together.)
  • [Low] Silent describe.skip (markdown-example-integration.spec.ts:467): the integration suite no-ops when www/kompendium.json is absent, with no skip annotation — invisible in CI output. Mitigated by always-run unit coverage of the same scenarios.
  • [Low] resolveTarget (markdown-inline-links.ts:135) keeps an https?:// branch that the upstream URL pre-pass has usually already consumed — harmless defensive overlap, slightly redundant.
  • Positives: Small named node-builder helpers and an explicit ReplacementNode union; the SKIP, index + replacements.length revisit is correct and avoids reprocessing inserted nodes; transformTextNode resets lastIndex defensively; comments explain the non-obvious decisions.

3. Architecture — GOOD ✅

Cleanly extends the existing pipeline. The new module is self-contained and the route-URL convention is centralized in createLinkResolver. Pipeline ordering is correct: inlineLinks runs on mdast before remarkRehype, and the hast-stage typeLinks deliberately skips the resulting anchored code via the new insideAnchor logic — the two passes are intentionally de-conflicted.

  • [Low] Route-shape duplication: #/type/${target} now appears in both createLinkResolver (markdown.ts:73) and the pre-existing createLinkNode (markdown-typelinks.ts:112). Pre-existing pattern; minor.
  • [Low] Two-phase URL handling (string pre-pass for URL targets vs mdast plugin for identifiers) splits one concern across two mechanisms with two near-duplicate regexes. The PR documents the rationale (remark-gfm autolink would tear URLs apart at parse time) and it is sound — deliberate, justified trade-off.
  • [Low] Module-level mutable registries (markdown-types.ts let components) populated from app.tsx and read in markdown.tsx. This faithfully extends the pre-existing let types singleton (not a new anti-pattern); the render-before-setComponents ordering hazard is pre-existing and degrades gracefully to inline code.
  • Positives: Self-contained new module, centralized resolver, correct mdast/hast ordering with intentional typeLinks de-confliction, graceful null-resolution fallback.

4. Security — SAFE ✅

No regression introduced. The pipeline already runs remarkRehype({ allowDangerousHtml: true }) + rehypeRaw with no sanitizer (proven by the unchanged "raw HTML passthrough" suite, markdown.spec.ts:555-575), so the trust model is "markdown authors are trusted" — developer-authored component JSDoc in a docs generator, not runtime end-user input. The new code does not widen that boundary.

  • [Low / pre-existing, not introduced here] No HTML/URL sanitizer in the pipeline (markdown.ts:50-51): authors can inject arbitrary HTML and javascript:/data: hrefs. Predates this PR; flagged as context. The new {@link} paths cannot add a dangerous-scheme sink — resolveTarget only emits #/type/…, #/component/…/, http(s)://, or null (unknown → plain text, no link), and the URL pre-pass hard-locks the scheme to http(s) via the capture group. A label containing raw HTML lands on the same pre-existing rehypeRaw sink that already accepts bare HTML — a more convoluted route to an existing capability, not a new exposure.
  • Positives: Unknown/non-http(s)/non-#/ targets are refused (rendered as plain text); the parent.type === 'link' guard prevents nested links; behavior is well covered including the unresolved-target and absolute-URL cases.

5. Observability — GOOD ✅

Appropriate for a build-time + client-side docs generator. No new logging, but consistent with the target branch and project conventions, and no previously-surfaced error is newly swallowed.

  • [Low] Unresolved {@link Target} renders as inline code with no warning (markdown-inline-links.ts:251-255; resolver markdown.ts:71-81 returns null). A typo'd type name gives a docs author no signal — but this exactly mirrors the existing typeLinks convention (markdown-typelinks.ts:90-98), where unknown types silently become text. Client-side logging would be inconsistent with the rest of the pipeline; a warning here is aspirational, not a regression.
  • [Low] Silent integration-test skip (markdown-example-integration.spec.ts:467) — same item as Code Quality; an observability-of-coverage concern. Mitigated by always-run unit tests.
  • [Low / not introduced here] The .process rejection propagates into renderMarkdown (markdown.tsx:52-57), which is async and called from lifecycle hooks with no .catch → unhandled rejection with no context. Pre-existing; the PR's new synchronous .replace pre-pass cannot throw on string input, so no new throw site is added.
  • Positives: Errors are correctly awaited and propagated, not caught-and-dropped; silent unresolved-reference fallback is a deliberate, documented design matching the existing convention; no sensitive-data-in-logs.

6. Performance — FAIR ⚠️

The Set-allocation and extra-tree-walk concerns are non-issues (dwarfed by the unified parse, and the visitor is gated by a cheap includes('{@link') check). But the regexes contain a genuine, reproduced backtracking cliff this PR introduces.

  • [High] ReDoS / catastrophic backtracking in URL_LINK_PATTERN (markdown-inline-links.ts:6-7) and INLINE_LINK_PATTERN (:41-42). The shared tail (?:\s*\|\s*([^}]+?)|\s+([^}]+?))?\s*\} blows up super-linearly on an unterminated {@link <target> followed by a long whitespace run — the leading \s+ of the second alternative and the boundary \s*\} backtrack over the same unanchored whitespace span. Independently reproduced: {@link X + 1 000 spaces ≈ 190 ms, + 2 000 ≈ 1.5 s, + 3 000 ≈ 5 s; a closing } or any non-whitespace after the target makes it ~0 ms. Runs synchronously on the render thread, so one bad string freezes the page. Trust boundary (developer-authored, build-time docs) makes it an accidental-DoS footgun rather than an exploitable attack surface — but it ships to all readers, and lime had no {@link handling at all, so this is net-new risk. Bounded fix: anchor/atomic the whitespace, require the optional label group to be non-empty, or cap target length.
  • Positives: Visitor gated by a cheap includes('{@link') check and skips text already inside links, so link-free docs pay near-zero; transformTextNode scans each text node once; the renderSeq stale-render guard is preserved.

7. Lime Platform Issues — SAFE ✅

Pure client-side markdown logic in a Stencil/TS doc generator (no Python backend, limetypes, hooks, tasks, or file I/O), so the server-side LPID rule families are out of scope; the frontend (FE) rules were applied to the changed .ts/.tsx and nothing matches.

  • No findings. The innerHTML write at markdown.tsx:64 (an LPID-FE-10 sink) is pre-existing and unchanged — context, not a diff addition — and the new code adds no unsanitized path into it (URLs come from the type/component registry or #/ fragments and flow through the standard remark-rehype stringifier). The module-level let components (markdown-types.ts:2) mirrors the existing let types; LPID-CC-9 (Global Mutable State) targets uwsgi cross-tenant worker memory and does not apply to a single-page browser app.
  • Positives: Resolver returns null for unknown targets (no dangling links); URL targets restricted to https?://, keeping javascript:/data: out of the autolink path; the skip-set generalisation correctly avoids nested-link generation.

Overall Verdicts

Dimension Verdict
Backward Compatibility SAFE
Code Quality GOOD
Architecture GOOD
Security SAFE
Observability GOOD
Performance FAIR
Lime Platform Issues SAFE
Merge Readiness BLOCKED

Top Recommendations

  1. [High from Performance] Fix the ReDoS in markdown-inline-links.ts:6-7, 41-42 before merge. The unbounded whitespace + unterminated-brace path is the trigger. Concrete options: require the closing brace to follow within a bounded distance, make the optional label group non-empty / anchor its leading whitespace, or cap target length. Add a regression test with an unterminated {@link X + long whitespace string asserting it returns promptly.
  2. [Low from Code Quality / Architecture] While fixing the regexes, factor out the shared label-alternation tail so URL_LINK_PATTERN and INLINE_LINK_PATTERN cannot drift, and consider building one from the other.
  3. [Low from Code Quality / Observability] Make the integration-test skip visible (markdown-example-integration.spec.ts:467) — a one-line note/warning on skip, or ensure CI builds kompendium.json before the test run, so the "real registry" coverage isn't silently absent.
  4. [Low from Architecture] Optionally centralize the #/type/${target} / #/component/${target}/ route-shape so it isn't duplicated between createLinkResolver and markdown-typelinks.ts:createLinkNode.

🤖 6-agent review via 6-agent-review skill. The single blocker is narrow and cheaply fixable; everything else is a net improvement over lime.

@adrianschmidt

Copy link
Copy Markdown
Author

Addressed the 6-agent review

Thanks for the thorough pass. Summary of what changed and what I left as-is.

Fixed the blocker: ReDoS in the {@link} regexes (6f7ac10, fixup of 131addb)

The old label-matching tail (?:\s*\|\s*([^}]+?)|\s+([^}]+?))?\s*\} could backtrack catastrophically on an unterminated {@link X followed by a long run of whitespace — measured at ~5 seconds for 3,000 spaces, running synchronously on the render thread.

I removed the in-pattern label alternation entirely. Both patterns now capture everything between the target and the closing brace in a single greedy ([^}]*) run that requires a literal }, then split the separator/label in code via a shared splitLabel() helper. This makes matching linear: the same 3,000-space input that took ~5 s now returns in ~0 ms, and an unterminated reference simply doesn't match (the text is left untouched).

A nice side effect: this also resolves the regex-duplication Low item — the two patterns previously shared a verbatim tail that had to stay in lockstep; now they share splitLabel() and can't drift.

Added regression tests covering both the mdast plugin and the URL pre-pass with a 50,000-space unterminated reference (each asserts it returns in <100 ms and leaves the input unchanged), plus direct unit coverage for normalizeInlineLinkUrls (bare URL, pipe label, space label, non-URL passthrough). The full behaviour matrix is preserved — verified by the existing suite still passing.

Made the silent integration-test skip visible (0b72fb8, fixup of 4e5284e)

The integration suite describe.skips when www/kompendium.json isn't built, which was invisible in CI. There's now an always-run guard that emits a console.warn explaining why it was skipped and how to enable it, so the absence of real-registry coverage is no longer silent.

Deliberately not changed

  • Redundant https?:// branch in resolveTarget — it's a cheap defensive net (and also handles the #/ fragment case in the same guard). Removing it would slightly reduce robustness for no real gain, so I kept it.
  • Route-shape duplication (#/type/${target}) — this is a pre-existing pattern shared with markdown-typelinks.ts; centralizing it would touch code outside this PR's scope for little benefit. Left for a separate cleanup if desired.

Verification

  • Tests: 121 passed, 6 skipped (127 total), 10 suites passed + 1 skipped (the integration suite, correctly skipped since the docs aren't built in this run).
  • Formatting: prettier clean on all changed files. Note: npm run lint:src's ESLint step is broken on this branch independent of these changes (a pre-existing ESLint 9 / eslint-plugin-sonarjs flat-config incompatibility — fails identically on a clean checkout); the prettier half passes and the code follows existing style conventions.

@adrianschmidt

Copy link
Copy Markdown
Author

Consolidated PR Review (re-review @ 0b72fb8)

Second pass after the fixups. Reviewed against target branch lime. The previous blocker (ReDoS) was the focus; its fix was verified, and a related residual was found and independently reproduced (numbers below).

PR Summary

Rewrites inline JSDoc/TSDoc {@link Target} references into clickable links via a remark plugin, resolving targets against the project's known types and component tags. Since the first review, the two {@link} regexes were rewritten from a backtracking label-alternation tail to a linear ([^}]*)\} capture with label parsing moved into a shared splitLabel() helper, ReDoS regression tests were added, and the integration-test silent-skip got a visible always-run guard. 11 files, +700/−20.

Merge Readiness — BLOCKED ❌

Most of the first review was addressed well: the whitespace-run ReDoS is genuinely gone, the regex duplication is resolved by splitLabel(), the silent test-skip is now visible, and the two "won't fix" Lows had sound reasoning. But the regex fix is incomplete — a different super-linear backtracking path remains and the new regression test does not cover it. Because it's still a net-new performance cliff on the render thread vs lime, it blocks merge. The fix is small and well-understood.

  • Blockers:
    1. [High — Performance] Residual O(n²) catastrophic backtracking in URL_LINK_PATTERN / INLINE_LINK_PATTERN (src/kompendium/markdown-inline-links.ts:13, 15). The adjacent overlapping quantifiers ([^\s}|]+)([^}]*)\} redistribute a long unbroken run between the two groups when no closing } follows. Independently reproduced on an unterminated {@link + long non-whitespace token: 10k chars = 43 ms, 50k = 1.0 s, 100k = 4.2 s, 200k = 16 s, 500k = 105 s (a terminated …} token of 100k = 0.09 ms). The whitespace fix works because a space terminates [^\s}|]+ after one char — but the regression test (markdown-inline-links.spec.ts:716-743) uses spaces, so it never exercises the long-target path and gives false confidence. Fix: make the target's right edge non-overlapping with the tail (e.g. require a delimiter (?=[\s|}]) after the target, or bound/anchor the tail), and add a regression test using a long unbroken token (not spaces).
  • Non-blocking but worth addressing: a few [Low] items (a URL-{@link}-inside-code passthrough gap, an empty-pipe edge case worth a test, an implicit cross-phase contract worth a comment). See Top Recommendations.

1. Backward Compatibility — SAFE ✅

Cleanly additive against lime. The rewritten regexes + splitLabel() reproduce the documented behaviour matrix; the skip-set refactor preserves behavior for markdown without {@link}.

  • [Low] A URL reference written inside inline/fenced code — `{@link https://x}` — is rewritten by the string pre-pass normalizeInlineLinkUrls (markdown.ts:37-39, markdown-inline-links.ts:51-57) rather than passed through, because the pre-pass runs over raw text before parsing. The in-code passthrough guarantee holds for non-URL targets (handled by the mdast plugin, which only visits text nodes) but not URL targets. This was present in the original feature commit, untouched by the fixups, and lime had no {@link} handling at all — a matrix gap, not a regression. Worth a follow-up test.
  • Positives: Purely additive API (markdownToHtml's 3rd param defaults to []); sole production call site updated; pre > code skip behavior preserved verbatim; the new anchor-nested-code skip is required by the feature and regresses no existing type links test.

2. Code Quality — GOOD ✅

The rewrite is correct and a clear net improvement: splitLabel() is a clean single-owner extraction, the patterns are genuinely linear in structure, and the bare→code / labeled→prose distinction is preserved. Regex-state handling is safe (INLINE_LINK_PATTERN.lastIndex reset in transformTextNode; URL_LINK_PATTERN used via .replace).

  • [Low] Behavioral divergence (an improvement, not a defect) on the malformed input {@link X |} (markdown-inline-links.ts:29-46): the old alternation captured the stray | as a prose label; the new splitLabel treats an empty pipe-label as bare (explicit:false) → code X. The new behavior is more correct but untested — adding the {@link X |} case would lock in the intended semantics, since it's exactly what the rewrite changed.
  • [Low] markdown-example-integration.spec.ts:523-525 calls data.docs.components.map(...) unguarded; a future built kompendium.json lacking components would throw rather than skip. Test-only, behind isBuilt; non-blocking.
  • Positives: Shared splitLabel removes the duplication cleanly; the insideAnchor typeLinks refactor correctly composes the two link passes; the ReDoS regression tests assert both timing and the no-rewrite invariant (good intent, even though they miss the residual path above).

3. Architecture — GOOD ✅

splitLabel() is extracted at the right seam: both patterns end in the identical ([^}]*)\} tail, so the rest argument has structurally-identical semantics in both call sites — genuine single-source-of-truth for the label grammar, not an out-of-band coupling.

  • [Low] Implicit cross-phase contract between inlineLinks (mdast, emits link > inlineCode) and typeLinks (hast, now skips anchor-nested <code> via insideAnchor, markdown-typelinks.ts:31-54). The de-confliction is correct, but the dependency spans two pipeline stages and two modules with no cross-reference. A one-line comment in collectSkippableCode explaining why anchor-nested code is skipped (because inlineLinks produces it) would make the contract self-documenting.
  • [Low] resolveTarget's https?:// branch (markdown-inline-links.ts:168-173) is effectively dead for the mdast path since the pre-pass already extracts URLs — the author documented this as a deliberate defensive net that also handles #/; sound, noted only for completeness.
  • Positives: Label semantics now have one owner; the linear-regex + in-code-parsing split is the right division between matching and grammar; registries and the resolver remain consistent with the existing pattern.

4. Security — SAFE ✅

The rewrite does not widen the injection surface versus the prior revision or lime.

  • No findings. resolveTarget (markdown-inline-links.ts:304-310) passes a target verbatim only for ^https?:// or #/; everything else goes through createLinkResolver, which emits only #/type/…, #/component/…/, or null — so {@link javascript:…} resolves to null and renders as inline code, no href. The URL pre-pass hard-locks its capture to https?://. mdast labels become escaped text/inlineCode nodes, not re-parsed markup. The pre-pass can synthesize a different markdown link from a crafted label, but only via a literal https?:// trigger and only as a more convoluted route to what an author can already write directly — the pipeline's no-sanitizer posture (allowDangerousHtml + rehypeRaw) is pre-existing and the trust model (developer-authored docs) is unchanged.
  • Positives: New scheme handling is narrower than hand-written markdown — schemes/anchors are whitelisted and the registry resolver emits only #/ routes; the linear regex removes the prior ReDoS class without opening any injection path.

5. Observability — GOOD ✅

The prior review's sole item — the silent describe.skip — is correctly addressed.

  • No findings. The new guard (markdown-example-integration.spec.ts:506-517) registers an always-run test that console.warns why the integration suite is skipped when www/kompendium.json is absent and asserts isBuilt === false, converting an invisible skip into visible, actionable reporter output. A malformed (vs absent) registry would surface loudly via a parse error — the correct failure mode. No new silent error-swallowing; the pre-existing fire-and-forget renderMarkdown rejection path is unchanged (only its arguments changed).
  • Positives: The skip-visibility fix is precise, commented, and actionable; the ReDoS regression tests add observable wall-clock signal where there was none.

6. Performance — FAIR ⚠️

The original whitespace-run ReDoS is genuinely fixed and splitLabel/normalizeInlineLinkUrls are linear, but the rewrite introduced/retained a different super-linear path the tests miss.

  • [High] Residual O(n²) backtracking in URL_LINK_PATTERN (markdown-inline-links.ts:13) and INLINE_LINK_PATTERN (:15). The classes [^\s}|] and [^}] in ([^\s}|]+)([^}]*)\} overlap, so when the mandatory closing \} can't match, the engine tries every partition of a long unbroken run between the two groups, re-scanning the tail each time. Reproduced: {@link + an unbroken non-whitespace/|/} token of 10k chars = 43 ms, 50k = 1.0 s, 100k = 4.2 s, 200k = 16 s, 500k = 105 s; the same token with a closing } is 0.09 ms (linear). Runs synchronously on the render thread. The trigger is narrower than the original whitespace cliff (needs a long unbroken token, and input is developer-authored docs), but it's a genuine net-new super-linear cliff vs lime, and the regression test (markdown-inline-links.spec.ts:716-743) uses spaces — which terminate [^\s}|]+ after one char — so it does not cover this path. Fix: require a delimiter after the target ((?=[\s|}])) or otherwise stop the target and tail from co-claiming the run; add a long-unbroken-token regression test.
  • Positives: Whitespace-run ReDoS eliminated; splitLabel/normalizeInlineLinkUrls strictly linear; visitor still gated on a cheap includes('{@link') and skips link-parented text; resolver Set lookups O(1); typeLinks refactor stays single-pass linear.

7. Lime Platform Issues — SAFE ✅

Client-side markdown logic in a Stencil/TS docs generator; the server-side LPID rule families are out of scope and the frontend rules don't fire.

  • No findings. LPID-FE-10 (XSS sink): not introduced — inlineLinks emits structured escaped mdast nodes, normalizeInlineLinkUrls constrains URLs to https?://, and the pre-existing innerHTML/allowDangerousHtml sink (markdown.tsx:64, markdown.ts:50-51) is unchanged and not widened. LPID-FE lifecycle/state rules: the example component is a pure render(), and app.tsx adds one synchronous setComponents call in the existing data-load path.
  • Positives: No Lime-platform runtime hazards introduced; ReDoS-resistant intent with an explicit (if incomplete) regression guard; no new XSS sink; the integration-test guard surfaces dropped coverage in CI.

Overall Verdicts

Dimension Verdict
Backward Compatibility SAFE
Code Quality GOOD
Architecture GOOD
Security SAFE
Observability GOOD
Performance FAIR
Lime Platform Issues SAFE
Merge Readiness BLOCKED

Top Recommendations

  1. [High from Performance] Eliminate the residual O(n²) in both {@link} regexes (markdown-inline-links.ts:13, 15). The adjacent overlapping quantifiers ([^\s}|]+)([^}]*) are the cause; require a delimiter after the target (e.g. \{@link\s+([^\s}|]+)(?=[\s|}])([^}]*)\}) or restructure so the target and tail can't redistribute a long run. Crucially, add a regression test that uses a long unbroken non-whitespace token (e.g. '{@link ' + 'a'.repeat(50000)), not spaces — the current 50k-space test passes without touching this path.
  2. [Low from Backward Compatibility] Add a test (and decide intended behavior) for a URL {@link https://…} written inside inline/fenced code — the pre-pass currently rewrites it rather than passing it through.
  3. [Low from Code Quality] Add a test for the {@link X |} empty-pipe-label case to lock in the (improved) bare-reference semantics the rewrite introduced.
  4. [Low from Architecture] Add a one-line comment in collectSkippableCode (markdown-typelinks.ts) noting that anchor-nested <code> is skipped because inlineLinks emits that shape — making the implicit cross-phase contract self-documenting.

🤖 6-agent re-review via 6-agent-review skill. The whitespace ReDoS and the other first-round items are addressed; the single remaining blocker is a narrower residual of the same backtracking class, reproduced above, with a small fix.

@adrianschmidt

Copy link
Copy Markdown
Author

Thanks for the careful re-review — you were right that the first fix was incomplete.

Fixed the residual backtracking cliff (commit 65dba7b, fixup of 131addb)

The two {@link} regexes had adjacent overlapping quantifiers — ([^\s}|]+)([^}]*)\} — where [^\s}|] is a subset of [^}]. On an unterminated {@link followed by a long unbroken token (no space, |, or }), the engine tried every partition of that run between the two groups before failing: O(n²).

Fix: a zero-width lookahead pins the target's right edge to a delimiter, so the target can't grow into the tail:

  • before: /\{@link\s+([^\s}|]+)([^}]*)\}/g
  • after: /\{@link\s+([^\s}|]+)(?=[\s}|])([^}]*)\}/g (URL variant analogously)

The rest capture (everything between target and }) is unchanged, so splitLabel() and the full behaviour matrix are preserved.

Measured — unterminated {@link + long unbroken token

input length old regex new regex
50,000 chars ~1,040 ms 0.19 ms
200,000 chars ~16 s (per review) 0.53 ms

URL pattern ({@link http://a…), 50,000 chars: ~1,030 ms → 0.15 ms. Now linear.

Tests

Added long-unbroken-token regression tests ('a'.repeat(50000), not spaces) for both the mdast plugin path and the normalizeInlineLinkUrls URL pre-pass, asserting prompt return (<100 ms) + no rewrite. The existing space-based tests stay. Full suite: 126 passed, 6 skipped (the skips are the registry-gated integration suite). ESLint + prettier clean.

Also addressed (the Low items)

  • Empty-pipe {@link X |}: added a test locking in the bare-reference semantics the rewrite introduced (identifier wrapped in code, never a literal |).
  • URL {@link} inside code: added two tests documenting this as a known, pre-existing gap — the URL pre-pass runs over raw text before parsing, so it can't distinguish a code span from prose and rewrites the URL even inside inline/fenced code. Non-URL references in code are still correctly passed through. Left as-is and documented rather than "fixed", since making the string pre-pass code-aware would mean reimplementing the parser's code-span detection; the input is developer-authored docs and this trade-off was already deliberate.
  • Cross-phase contract comment: added a comment in collectSkippableCode explaining anchor-nested <code> is skipped because inlineLinks emits that link > inlineCode shape.

All four changes are in the single fixup 65dba7b, since they all belong to the feature commit's files.

@adrianschmidt

Copy link
Copy Markdown
Author

Consolidated PR Review (3rd pass @ 65dba7b)

Third pass. Reviewed against target branch lime. The two prior blockers were both ReDoS in the {@link} regexes; this pass verifies the lookahead fix and re-probes for residuals (reproduced numbers below).

PR Summary

Rewrites inline JSDoc/TSDoc {@link Target} references into clickable links via a remark plugin, resolving against the project's types and component tags. Since the last pass, a zero-width lookahead (?=[\s}|]) was added to both {@link} regexes to eliminate the residual O(n²), a long-unbroken-token regression test was added (the prior space-based test missed the path), and several [Low] follow-ups were addressed (empty-pipe test, URL-in-code passthrough documentation tests, a cross-phase contract comment). 11 files, +700/−20 (approx).

Merge Readiness — MERGE WITH CAVEATS ⚠️

The blocker is resolved. The single-token O(n²) that blocked the prior two passes is genuinely gone: the lookahead pins the target's right edge so the two quantifiers can no longer redistribute a long run — verified linear to 1M chars across every adversarial shape (unbroken token, token+delimiter+token, embedded pipes, huge terminated input). No dimension shows a regression vs lime; the PR is a clear net improvement. What remains is a set of [Low] follow-ups, none of which gate merge — including one much milder, practically-unreachable residual quadratic noted below for completeness.

  • Blockers: None. The prior ReDoS blocker is fixed and independently verified.
  • Non-blocking but worth addressing: A [Low] residual quadratic on pathological terminator-free input (Performance), plus minor [Low] items in Code Quality, Architecture, and Observability. See Top Recommendations.

1. Backward Compatibility — SAFE ✅

The lookahead is provably behaviour-neutral, and the change remains purely additive.

  • No findings. The target class [^\s}|]+ already excludes whitespace, }, and |, so the character after a target run is necessarily one of those (or end-of-input); the lookahead (?=[\s}|]) therefore only fails on an unterminated target — exactly the malformed case it targets. Every documented form ({@link X}, space/pipe labels, empty-pipe, hash-route, all URL variants) matches identically to before. markdownToHtml's 3rd param stays optional/defaulted; the sole production call site is updated; typeLinks renames are internal with no external referents.
  • Positives: Lookahead is behaviour-neutral (target class already excludes the delimiter set); signature change strictly additive; type-links changes are internal renames plus an additive, insideAnchor-guarded skip.

2. Code Quality — GOOD ✅

The new code is small, focused, and well-executed; the lookahead carries an unusually clear comment naming the failure mode and mechanism.

  • [Low] The ReDoS regression tests use a wall-clock toBeLessThan(100) assertion (markdown-inline-links.spec.ts:~733, 744, 765, 773). Pragmatic and not flaky in practice (post-fix is sub-millisecond on 50k chars; failure mode is multi-second, ~20× margin), but absolute wall-clock thresholds are a theoretical flake source under extreme CI starvation. Acceptable as-is.
  • Positives: splitLabel correctly parses the tail in code rather than regex; regex-state handling is safe (lastIndex reset in transformTextNode; URL_LINK_PATTERN only via .replace); tests meaningfully cover the previously-missed long-unbroken-token path plus the empty-pipe edge and both code-passthrough paths.

3. Architecture — GOOD ✅

Changes since the prior (GOOD) pass are localized and sound; the previously-implicit inlineLinkstypeLinks cross-phase contract is now documented at the consuming seam.

  • [Low] The tagName === 'code' && insideAnchor skip (markdown-typelinks.ts:~46) is broader than the documented contract — it suppresses re-linking for any <code> inside any <a>, not only inlineLinks-produced nodes. This is correct (you never want a nested anchor regardless of origin), so it's an observation, not a defect; the comment could note the broader-than-stated scope. Cosmetic.
  • Positives: Coherent renames and the addPreCodeChildren extraction keep the traversal readable; the cross-phase ordering contract is documented at the right seam with the producer named; the lookahead is confined to regex internals without disturbing the already-justified module structure.

4. Security — SAFE ✅

The lookahead is a zero-width assertion that doesn't relax https?://, alter either capture's character class, or widen the URL pre-pass capture (it asserts true exactly at the pre-existing target boundary). No new scheme/injection path.

  • No findings. resolveTarget still refuses non-http(s)/non-#/ targets (a javascript:/data: target → null → inert inline code); normalizeInlineLinkUrls still hard-locks hrefs to https?://; mdast labels are escaped, not re-parsed. The pre-existing allowDangerousHtml/rehypeRaw no-sanitizer posture is unchanged and not widened (context only — trust model is developer-authored docs).
  • Positives: Exact-match Set resolver + http(s)/#/-only gate make scheme injection through {@link} structurally impossible; the lookahead is capture-preserving DoS hardening that also closed the prior residual.

5. Observability — GOOD ✅

No new silent error-swallowing; no previously-surfaced signal removed. The visible integration-test skip guard from pass 1 is intact.

  • [Low] app.tsx:~86-87this.data.docs.components?.map(...) ?? [] silently yields zero components if docs.components is absent, so every {@link} to a real component would quietly fall back to inline code. Acceptable for an optional field in build-generated JSON (a missing collection is legitimately empty, not an error), and the enclosing fetchData() still console.errors genuine failures. Noted for completeness; server-grade diagnostics would be aspirational here.
  • Positives: Unresolved refs degrade visibly to inline code, not silently; the skip guard surfaces dropped coverage in CI; ReDoS timing assertions give CI a loud signal if linearity regresses.

6. Performance — GOOD ✅

The lookahead genuinely resolves the previously-blocked single-token O(n²): after [^\s}|]+ consumes a run, the lookahead fails O(1) on every backtracked position (each exposed char is a non-delimiter) and never hands off to the tail. Long unbroken token, token+delimiter+long tail, and terminated inputs are all linear — verified to 1M chars.

  • [Low] A different, much milder super-linear path survives (markdown-inline-links.ts:16, 18): a }-free span containing many {@link near-misses. A single exec over such a string attempts a match at each {@link start, each scanning to EOS, giving O(n²) across starts — distinct from the blocked overlapping-quantifier redistribution (per-start cost here is linear). Reproduced: ~88 KB of pure {@link a with zero } anywhere = 357 ms; 176 KB = 1.4 s. But it's practically unreachable: any } in the text node defuses it (195 KB of terminated tokens = 1.5 ms), realistic 2 KB prose with a stray {@link = 0.02 ms, and the constant is tiny (one char-class scan). Component descriptions don't look like this, so it does not rise to a blocker — but a belt-and-suspenders bound (e.g. stop the tail at newlines via [^}\n]*, or cap target/tail length) would flatten it. Rated [Low] deliberately: quadratic not exponential, tiny constant, trivially defused, no realistic trigger.
  • Positives: The blocked single-token cliff is genuinely closed; splitLabel/normalizeInlineLinkUrls are linear; the common terminated path is O(n); the visitor is gated by includes('{@link') and skips link parents; resolver Sets allocate once per call; typeLinks stays single-pass.

7. Lime Platform Issues — SAFE ✅

Frontend Stencil/TS markdown code; server-side LPID families are out of scope and the frontend rules don't fire.

  • No findings. LPID-FE-10 (XSS sink): not introduced — inlineLinks emits structured escaped mdast nodes, the URL pre-pass emits markdown source routed back through the pipeline, resolveTarget excludes javascript:, and the pre-existing innerHTML/allowDangerousHtml sink is untouched/unwidened. LPID-SEC-26 (ReDoS): Python-re-scoped and, regardless, this change removes backtracking rather than adding it. Lifecycle/state/HTTP rules don't apply to the changed code.
  • Positives: ReDoS-resistant regex with regression tests; unresolved targets degrade to inline code (never dangling links); nested-link and code-inside-<a> guards prevent double-wrapping; URL allow-listing avoids unsafe schemes.

Overall Verdicts

Dimension Verdict
Backward Compatibility SAFE
Code Quality GOOD
Architecture GOOD
Security SAFE
Observability GOOD
Performance GOOD
Lime Platform Issues SAFE
Merge Readiness MERGE WITH CAVEATS

Top Recommendations

  1. [Low from Performance] Optional belt-and-suspenders for the residual many-near-misses quadratic (markdown-inline-links.ts:16, 18): bound the tail so a missing terminator can't drive an O(n²) multi-start scan — e.g. stop the tail at newlines ([^}\n]*, which also matches how {@link} is used on single lines) or cap target/tail length. Add a regression test using many {@link near-misses with no }. Not a blocker; realistic docs never trigger it and any } defuses it.
  2. [Low from Architecture] Optionally note in the collectSkippableCode comment that the anchor-nested-<code> skip is intentionally broader than the inlineLinks contract (it suppresses re-linking for any <code> inside any <a>).
  3. [Low from Code Quality] Consider hardening the wall-clock ReDoS timing assertions against CI starvation (e.g. a more generous bound, or measuring relative to a control) — low priority.
  4. [Low from Observability] If desired, surface a one-time dev console.warn when docs.components is absent so a missing registry doesn't silently turn every component {@link} into inline code — optional for a build-time tool.

🤖 6-agent re-review via 6-agent-review skill. The ReDoS blocker from the prior two passes is resolved and verified; remaining items are all [Low] and do not gate merge.

@adrianschmidt

Copy link
Copy Markdown
Author

Thanks for the third pass. With no blockers, I went through the four [Low] items and acted on the two I judged genuinely worth it; the other two I deliberately left alone (reasoning below).

Addressed

1. Residual many-near-misses quadratic (Performance) — fixed and verified linear.
I bounded the tail in both {@link} regexes from ([^}]*) to ([^}\n]{0,1000}): it stops at newlines (an inline {@link} never spans lines) and caps the label at 1000 chars. I went a step beyond the suggested [^}\n]* because measurement showed a newline-stop alone only flattens the multi-line shape — a single giant line of near-misses stays quadratic (and is even marginally slower). The length cap is what makes every start's scan bounded, so the whole pass is linear regardless of shape.

Measured (raw exec, before vs after this change):

Input Length Before After
Single-line near-misses 90 KB / 180 KB / 360 KB 370 / 1465 / 5844 ms 19 / 38 / 76 ms
Multi-line near-misses 100 KB / 200 KB / 400 KB 409 / 1627 / 6507 ms 0.2 / 0.3 / 0.7 ms

Before scales O(n²) (4x the time per 2x the length); after scales linearly. Added three regression tests (single-line, multi-line, URL pre-pass) that fail fast against this shape, and re-confirmed the full behaviour matrix and all existing tests still pass. No real reference has a >1000-char label or crosses a line, so this is behaviour-neutral for actual docs.

2. Anchor-nested-<code> skip comment (Architecture) — done.
Extended the collectSkippableCode comment to note the guard is intentionally broader than the inlineLinks contract: it suppresses re-linking for any <code> inside any <a>, regardless of origin, since a nested anchor is never wanted.

Deliberately skipped

3. Hardening the wall-clock timing assertions (Code Quality). The existing toBeLessThan(100) bounds have ~20x headroom over the sub-millisecond post-fix runtime, and the failure mode is multi-second, so they aren't flaky in practice. Swapping to a relative/control-based measurement trades a theoretical starvation flake for a different kind of timing fragility with no clear win. Not worth the churn.

4. Dev console.warn when docs.components is absent (Observability). This is a build-time tool and a missing optional collection is legitimately empty, not an error; the enclosing fetchData() already logs genuine failures. Adding a warning here felt speculative for no concrete payoff.

Verification

  • Tests: 129 passed, 6 skipped (the skip is the pre-existing integration-test guard, unrelated). The three new near-miss tests pass: single-line 45 ms, multi-line 1 ms, URL pre-pass 55 ms — all comfortably under the 100 ms bound; the single-line case was ~1.4 s before the cap.
  • Lint (lint:src): clean.

Single fixup commit 4b98498 targeting the feat commit 131addb (all three changed files belong to that commit). No autosquash.

@adrianschmidt

Copy link
Copy Markdown
Author

Consolidated PR Review (4th pass @ 4b98498)

Fourth pass, against target branch lime. The recurring concern across passes was ReDoS in the {@link} regexes; this pass confirms the class is now fully closed and finds no actionable issue in any dimension.

PR Summary

Rewrites inline JSDoc/TSDoc {@link Target} references into clickable links via a remark plugin, resolving against the project's types and component tags. Since the prior pass, the regex tail was bounded from ([^}]*) to ([^}\n]{0,1000}) — stopping at newlines and capping length — which makes the last residual (many-near-misses) quadratic linear, with three regression tests added. 11 files.

Merge Readiness — READY TO MERGE ✅

No regressions in any dimension; the PR is strictly better than lime. The ReDoS class that blocked passes 1–2 and left a [Low] residual in pass 3 is now fully closed — independently verified linear across single-token, single-line many-near-misses, multi-line, and embedded-pipe shapes (e.g. 360 KB single-line near-misses: 5.8 s → 78 ms). All prior [Low] follow-ups worth acting on were addressed; the two deliberately skipped (timing-assertion hardening, docs.components warn) had sound reasoning. The remaining observations are cosmetic and need no action.

  • Blockers: None.
  • Non-blocking: Only cosmetic notes (see below); nothing that should gate merge.

1. Backward Compatibility — SAFE ✅

No findings. markdownToHtml's third param stays optional/defaulted (sole production call site updated; legacy calls still compile); the bounded tail breaks no realistic {@link} form (every documented form is short and single-line; the target is captured outside the bounded tail, so long identifiers are unaffected); the typeLinks refactor preserves pre > code skipping and fixes a latent nested-anchor case. The >1000-char/newline non-match is a deliberate, documented trade-off, not a realistic regression.

  • Positives: Purely additive optional param; bounded regex preserves every realistic form while closing the ReDoS class; thorough new test coverage with no changes to existing assertions.

2. Code Quality — GOOD ✅

No findings. The {0,1000} bound is the correct linear-time fix (bounded greedy class + literal }, no off-by-one, no new backtracking), the magic number is justified in-comment, and the three new tests target the exact super-linear shapes (single-line, multi-line, URL pre-pass) asserting both prompt return and unchanged output.

  • Positives: Correct bound with accurate, well-matched comments; regression tests pin behavior, not just timing.

3. Architecture — GOOD ✅

No structural concerns. The components registry faithfully mirrors the existing types registry (no new architectural axis); collectSkippableCode cleanly generalizes the old pre > code collector into an anchor-aware DFS that enforces the inlineLinkstypeLinks contract structurally; the regex bound is matching-internal.

  • [Low] A pre > code node that is also inside an <a> could be added to the skip Set twice (markdown-typelinks.ts:~50, ~64) — harmless (Set dedups; idempotent). Cosmetic, no action needed.
  • Positives: Registry mirrors the existing pattern (zero new abstraction); the two-pass contract is enforced by structure rather than ordering luck.

4. Security — SAFE ✅

No regression. Narrowing the tail is purely restrictive and the tail feeds only the display label, never the URL/scheme. resolveTarget still refuses non-http(s)/non-#/ targets, the URL pre-pass hard-locks https?://, and the non-URL path emits escaped mdast nodes.

  • [Low — pre-existing, not introduced/widened here] The URL pre-pass interpolates the label into raw markdown [label](url) and the pipeline has no sanitizer (allowDangerousHtml, no rehype-sanitize) — but lime already ships this exact pipeline and the input is developer-authored docs (a doc author can already write raw <script> / [x](javascript:…) directly). No new untrusted channel. Worth hardening someday (escape ]/( in the label), independent of this PR.
  • Positives: Defense-in-depth scheme lockdown on both paths; the non-URL path uses escaped mdast nodes rather than string interpolation.

5. Observability — GOOD ✅

No findings. The delta touches no logging/error path; setComponents rides the existing console.error catch in fetchData; the integration-test skip remains loudly logged (a signal gain from pass 1); the tail-bound trade-off degrades visibly to literal text.

  • Positives: No new silent failure paths; the regex trade-off degrades to visible prose.

6. Performance — SAFE ✅

The ReDoS class is fully closed. Rigorous per-start analysis confirms the whole pass is O(n): the bounded tail caps each exec start's scan at 1000 chars (many-near-misses → linear), the lookahead still kills single-token redistribution, and the two worst-case cost terms (long per-start target backoff vs. many starts) are structurally mutually exclusive over any region — so linearity is robust, not coincidental. Embedded-pipe and multi-line shapes reduce to the same bounded case.

  • No actionable findings. The ≤1000-char/single-line bound is the documented intended trade-off.
  • Positives: ReDoS class fully closed and provably linear; cheap includes('{@link') short-circuit keeps the common path off the regex; resolver Sets and tree walks are single-allocation / single-pass.

7. Lime Platform Issues — SAFE ✅

No findings. Frontend Stencil/TS docs-generator code; server-side LPID families are out of scope. LPID-SEC-26 (ReDoS) explicitly excludes constant source regex literals — and this change removes backtracking with regression coverage. LPID-FE rules don't fire (structured mdast nodes, no new innerHTML/fetch/lifecycle/state).

  • Positives: Well-reasoned, documented regex hardening with regression tests for all three super-linear shapes; unresolved refs degrade to inline code, never dangling links.

Overall Verdicts

Dimension Verdict
Backward Compatibility SAFE
Code Quality GOOD
Architecture GOOD
Security SAFE
Observability GOOD
Performance SAFE
Lime Platform Issues SAFE
Merge Readiness READY TO MERGE

Top Recommendations

No actionable items remain. The blocker from passes 1–2 and the residual from pass 3 are resolved and verified. The only open notes are optional/cosmetic and need not gate merge:

  1. [Low from Security, pre-existing] Independent of this PR, the markdown pipeline has no sanitizer; if kompendium markdown ever accepts untrusted input, add rehype-sanitize. Not introduced here.
  2. [Low from Architecture, cosmetic] The double-add to the skip Set in collectSkippableCode is harmless; no change needed.

🤖 6-agent re-review via 6-agent-review skill. Clean across all dimensions — recommend merge.

@adrianschmidt

Copy link
Copy Markdown
Author

Thanks for the thorough 4th pass — agreed, this is ready to merge. I'm not making any further changes.

On the two remaining cosmetic notes, I confirm neither warrants action:

  • No HTML sanitizer (Low, Security, pre-existing): Not introduced or widened by this PR. The typeLinks pass emits structured mdast/element nodes rather than raw HTML, and the pipeline already shipped this exact shape in lime. Worth a rehype-sanitize someday if kompendium ever ingests untrusted markdown, but that's independent of this change.
  • Double-add to the skip Set (Low, Architecture, cosmetic): A pre > code that also sits inside an <a> can be added to the Set via both branches in collectSkippableCode. Since the set is keyed by node identity, the second add is idempotent — harmless and dedups by design. The two branches encode two independently-correct rules; collapsing them would couple them for no benefit. Leaving as-is.

No new commits, tip unchanged.

@adrianschmidt

Copy link
Copy Markdown
Author

Fixed the failing CI and Lint checks

Two real failures (both, fittingly, were called out as [Low] risks in the reviews above):

  • Lint (60f82fe… actually ee32ee1, fixup of 4e5284e): removed an unused // eslint-disable-next-line no-console directive in markdown-example-integration.spec.ts. The test-files override in eslint.config.mjs sets no-console: off, so the directive was unused — and lint:prod runs --max-warnings=0, so the "unused directive" warning failed the check.
  • CI / tests (60f82fe, fixup of 131addb): the three "many near-misses" ReDoS regression tests used a toBeLessThan(100) wall-clock bound that was too tight for a loaded CI runner (it measured ~327 ms there for the linear pass on a 180 KB input). The regex is genuinely linear; the assertion just needed hardening. Bumped the input to 40 000 tokens and the budget to 3 000 ms — sized so a quadratic regression takes several seconds (~5.7 s locally) and fails loudly in both local and CI environments, while the linear pass clears the bound comfortably everywhere.

Local npm test → 129 passed, 6 skipped, 0 failed. npm run lint:prod → clean.

The Block Autosquash Commits check is expected to stay red by design — it gates merge until the fixup! commits are squashed, which happens at merge time.

Kiarokh and others added 3 commits June 17, 2026 15:09
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>
@adrianschmidt
adrianschmidt force-pushed the feat/inline-link-references branch from ee32ee1 to 8d14c39 Compare June 17, 2026 13:09
@adrianschmidt
adrianschmidt marked this pull request as ready for review June 17, 2026 13:10
@adrianschmidt

Copy link
Copy Markdown
Author

Excellent work @Kiarokh!

@adrianschmidt
adrianschmidt merged commit 9c164fd into lime Jun 17, 2026
4 checks passed
@adrianschmidt
adrianschmidt deleted the feat/inline-link-references branch June 17, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants