Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/api-model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export { extractJulia } from './languages/julia.ts'
export { extractFSharp, extractFSharpArtifact } from './languages/fsharp.ts'
export { extractProject } from './project.ts'
export { resolvePublicIds } from './reexports.ts'
export { SymbolIndex, parameterId, type LinkTarget, type KeywordHelp } from './link.ts'
export { SymbolIndex, keywordArgument, parameterId, type LinkTarget, type KeywordHelp } from './link.ts'
export { tokenizeDoc, docSummaryText, referencesIn, type DocSpan } from './doc/roles.ts'
export { writeInventory, readInventory, type InventoryEntry } from './inventory.ts'
export { Resolver, notASymbol, toPath, DEFAULT_TEMPLATES, DEFAULT_PRIMARY_MODULES } from './resolver.ts'
Expand Down
14 changes: 14 additions & 0 deletions packages/api-model/src/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ const KEYWORD_HELP: Record<string, Record<string, KeywordHelp>> = {
},
}

/** A keyword argument (`cancel:`, `clients: true`) names a parameter, not a member. */
export const keywordArgument = /^[A-Za-z_]\w*[?!]?:(?:\s|$)/

interface TypeSpan {
text: string
link?: LinkTarget
Expand Down Expand Up @@ -677,6 +680,17 @@ export class SymbolIndex {
)
}

/**
* Link the names in a prose code literal.
*
* A keyword argument such as `cancel:` or `clients: true` names a parameter
* of the call being described, not the member elsewhere that shares its
* name, so it stays plain code.
*/
linkLiteral(text: string, context?: ApiSymbol): TypeSpan[] {
return keywordArgument.test(text) ? [{ text }] : this.linkType(text, context)
}

/**
* Split a type annotation into linked and literal spans.
*
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/api/ApiDoc.astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const labelParts = (label: string, qualified: boolean) => qualified ? label.spli
<span>{span.label}</span>
)
}
const parts = span.link ? [] : index.linkType(span.label, context)
const parts = span.link ? [] : index.linkLiteral(span.label, context)
const qualified = memberReference.test(span.label) && Boolean(span.link || parts.some((part) => part.link))
return span.link ? (
<a class="api-xref" href={span.link.href} rel={span.link.external ? 'nofollow noopener' : undefined}>
Expand Down
3 changes: 2 additions & 1 deletion site/src/lib/symbol-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ApiModel, ApiSymbol, Signature, SourceApiModel, SymbolIndex } from '@libtmux/api-model'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
import { membersOf, memberSignals, memberTier, moduleOf, qualifiedNameOf } from '@libtmux/api-model'
import { keywordArgument, membersOf, memberSignals, memberTier, moduleOf, qualifiedNameOf } from '@libtmux/api-model'
import mentions from '../data/mentions.json'
import { PORT_NAME, referenceAlternatives } from './api-models'
import { apiCommandFacts, apiCommandRelatedLinks, apiEntryFields, apiMemberGroups, apiMethodDetails, anonymousDeclarationLabel, hasScalarMembers, nativeSignatureText, parameterBindingText, signatureMultiline, signatureParameterText, usesNativeSignatures } from './api-sections'
Expand Down Expand Up @@ -85,6 +85,7 @@ function proseMarkdown(markdown: string, index: SymbolIndex, symbol: ApiSymbol,
}
if (!linkApi) return
if (node.type === 'inlineCode') {
if (keywordArgument.test(node.value)) return
if (!index.linkText(markdown.slice(start, end), symbol).some((span) => span.kind === 'ref')) return
const link = index.resolve(node.value, 'any', symbol)
if (link) edits.push({ start, end, text: `[${inlineCode(node.value)}](${link.href})` })
Expand Down
19 changes: 19 additions & 0 deletions site/test/inline-reference-appearance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { API_MODELS } from '../src/lib/api-models'
import { productApiIndex } from '../src/lib/product-api'
import { linkTmuxDoc } from '../src/lib/tmux-links'
import { symbolMarkdown } from '../src/lib/symbol-markdown'
import { createLinkPreviewPolicy } from '../src/lib/link-preview-paint'

const preview = readFileSync(new URL('../src/components/LinkPreview.astro', import.meta.url), 'utf8')
Expand Down Expand Up @@ -131,6 +132,24 @@ it('renders the WhereOf generic as one code expression with independent native l
}
})

it('keeps a keyword argument plain while its bare name still links', async () => {
const model = API_MODELS.ruby
const index = productApiIndex(model, 'latest')
const container = await experimental_AstroContainer.create()
const text = '`cancel:` `clients: true` `where:` `cancel`'
win.document.body.innerHTML = await container.renderToString(await docComponent(), { props: { text, index } })
const codes = [...win.document.querySelectorAll('code.api-literal')]
expect(codes.map((code) => code.textContent)).toEqual(['cancel:', 'clients: true', 'where:', 'cancel'])
expect(codes.slice(0, 3).map((code) => code.querySelectorAll('a').length)).toEqual([0, 0, 0])
expect(win.document.querySelectorAll('a[href*="cancellation-cancel"]')).toHaveLength(1)
// Markdown links code literals for TypeScript; the same rule keeps an option key plain there.
const ts = API_MODELS.ts
const summary = '`socketPath:` `socketPath`'
const server = { ...ts.symbols.find((entry) => entry.id === 'server.Server')!, doc: { summary } }
const markdown = symbolMarkdown({ model: ts, symbol: server, index: productApiIndex(ts, 'latest') })
expect(markdown).toContain('`socketPath:` [`socketPath`](')
})

it('leaves unrelated hashes, source comments and URL fragments unchanged', async () => {
const model = API_MODELS.scala
const index = productApiIndex(model, 'latest')
Expand Down
Loading