Skip to content
Merged
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
38 changes: 35 additions & 3 deletions scripts/check-source-links.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
* normalisation had introduced, and three ports naming a commit that exists
* only on a private fork — and both were invisible in a green build.
*
* Checked against git rather than HTTP so it runs offline, in CI, and without
* rate limits. git is also the stronger check: it can say the path exists *at
* that commit*, which a 200 from a moved file would not.
* Git links are checked against their commit without HTTP or rate limits.
* Generated sources use owned raw routes, checked against the embedded text
* those routes serve rather than a file that need not be committed.
*
* Usage: node scripts/check-source-links.mjs [--port py]
*/
Expand Down Expand Up @@ -89,19 +89,51 @@ for (const [port, where] of Object.entries(CHECKOUT)) {
}

const model = JSON.parse(readFileSync(modelPath, 'utf8'))
const generatedLengths = new Map()
const beforeGenerated = failed
if (model.generatedSources !== undefined) {
if (!model.generatedSources || typeof model.generatedSources !== 'object' || Array.isArray(model.generatedSources)) {
report(port, 'generatedSources must map relative source paths to text')
} else {
for (const [file, content] of Object.entries(model.generatedSources)) {
generatedLengths.set(file, null)
if (!file || /[\\?#%\p{Cc}]/u.test(file) || /^[a-z]:/i.test(file) ||
file.split('/').some((part) => !part || part === '.' || part === '..')) {
report(port, `generated source ${JSON.stringify(file)} is not a canonical relative path`)
} else if (typeof content !== 'string' || !content.trim() || content.includes('\0')) {
report(port, `generated source ${JSON.stringify(file)} must contain nonempty text`)
} else {
generatedLengths.set(file, content.split('\n').length)
}
}
}
}
const units = new Map()
let generatedLocations = 0
const locations = model.symbols.flatMap((symbol) => [symbol,
...symbol.signatures.filter((signature) => signature.source).map((signature, index) => ({
id: `${symbol.id} signature ${index + 1}`, source: signature.source,
})),
])
for (const symbol of locations) {
if (generatedLengths.has(symbol.source.file)) {
const length = generatedLengths.get(symbol.source.file)
const line = symbol.source.line
if (length !== null && line !== undefined && (!Number.isInteger(line) || line < 1 || line > length)) {
report(port, `${symbol.id} points outside embedded generated source ${JSON.stringify(symbol.source.file)}`)
}
generatedLocations++
continue
}
const repository = symbol.source.repo ?? model.repo
const revision = symbol.source.revision ?? model.revision
const key = `${repository}@${revision}`
if (!units.has(key)) units.set(key, { repository, revision, symbols: [] })
units.get(key).symbols.push(symbol)
}
if (generatedLengths.size && failed === beforeGenerated) {
console.log(`check-source-links: ${port} ${generatedLocations} generated declaration and method locations, ${generatedLengths.size} embedded files`)
}
for (const unit of units.values()) {
const repo = expand(PRODUCT_CHECKOUT[unit.repository] ?? where)
const rev = unit.revision
Expand Down
6 changes: 3 additions & 3 deletions site/src/components/MobileToC.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const { tocItems } = Astro.props
---

<!-- Mobile ToC Overlay -->
<div id="mobile-toc-overlay" class="fixed inset-0 bg-black/50 z-40 opacity-0 invisible lg:hidden"></div>
<div id="mobile-toc-overlay" class="fixed inset-0 bg-black/50 z-40 opacity-0 invisible xl:hidden"></div>

<!-- Mobile ToC -->
<aside id="mobile-toc" inert class="drawer-surface fixed right-0 top-0 h-full w-64 z-50 transform translate-x-full lg:hidden overflow-y-auto">
<aside id="mobile-toc" inert class="drawer-surface fixed right-0 top-0 h-full w-64 z-50 transform translate-x-full xl:hidden overflow-y-auto">
<div class="p-4">
<!-- Header with close button -->
<div class="flex items-center justify-between mb-6">
Expand Down Expand Up @@ -112,7 +112,7 @@ const { tocItems } = Astro.props

// Widening past the breakpoint hides the panel but would leave the body
// scroll-locked; see the sidebar drawer's note.
const desktop = window.matchMedia('(min-width: 1024px)')
const desktop = window.matchMedia('(min-width: 80rem)')
desktop.addEventListener('change', (e) => e.matches && closeToC(), { signal })
if (desktop.matches) closeToC()
}
Expand Down
9 changes: 3 additions & 6 deletions site/src/components/api/ApiGlossary.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@ const referenceBase = port ? referenceUrl(PORT_BY_SLUG[port], version, product ?
const pagePath = product ? `${product}/reference/glossary` : 'reference/glossary'
const glossaryPath = `${referenceBase}glossary/`
import { CONTRIBUTE_BRANCH, CONTRIBUTE_REPO } from '../../i18n/contribute'
import { apiGlossary, API_GLOSSARY_PATH, API_GLOSSARY_TITLE } from '../../lib/api-glossary'
import { apiGlossary, apiGlossaryHeadings, API_GLOSSARY_PATH, API_GLOSSARY_TITLE } from '../../lib/api-glossary'

const { title, description, groups } = apiGlossary(glossaryPath)
const headings = groups.flatMap((group) => [
{ depth: 2, slug: group.id, text: group.title },
...group.terms.map((term) => ({ depth: 3, slug: term.id, text: term.id })),
])
const { title, description } = apiGlossary(glossaryPath)
const headings = apiGlossaryHeadings(glossaryPath)
const markdownHref = `${glossaryPath.slice(0, -1)}.md`
---

Expand Down
4 changes: 2 additions & 2 deletions site/src/components/api/ApiGlossaryContent.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const jsonHref = `${glossary.path.slice(0, -1)}.json`
<p>{glossary.description} Select an annotation or full API badge to open its definition here.</p>
<p>The signature and entry documentation describe the API’s behavior, including its calling conventions, storage and runtime checks.</p>
{groups.map((group) => <section aria-labelledby={group.id}>
<h2 id={group.id}>{group.title}</h2>
<h2 id={group.id}>{group.title}<a class="api-heading-link" href={`#${group.id}`} aria-label={`Link to ${group.title}`} data-pagefind-ignore="all">#</a></h2>
<p>{group.description}</p>
{group.terms.map((term) => <section aria-labelledby={term.id}>
<h3 id={term.id}><code>{term.id}</code></h3>
<h3 id={term.id}><code>{term.id}</code><a class="api-heading-link" href={`#${term.id}`} aria-label={`Link to ${term.id}`} data-pagefind-ignore="all">#</a></h3>
<p>{term.definition}</p>
{term.detail && <p>{term.detail}</p>}
<p>References: {term.references.map((reference, index) => <>
Expand Down
24 changes: 18 additions & 6 deletions site/src/components/api/ApiReference.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ApiRelationships from './ApiRelationships.astro'
import { apiRelationshipSections, type ApiRelationshipPath } from '../../lib/api-relationships'
import ApiBadge from '../../components/api/ApiBadge.astro'
import ApiGlossaryContent from './ApiGlossaryContent.astro'
import { apiGlossary, API_GLOSSARY_TITLE, apiGlossaryHref } from '../../lib/api-glossary'
import { apiGlossary, apiGlossaryHeadings, API_GLOSSARY_TITLE, apiGlossaryHref } from '../../lib/api-glossary'
import ApiQualifiedName from '../../components/api/ApiQualifiedName.astro'
import ApiType from '../../components/api/ApiType.astro'
import ApiPageContents from '../../components/api/ApiPageContents.astro'
Expand Down Expand Up @@ -346,7 +346,10 @@ const memberGroups = apiMemberGroups(listed, signals)
const scalarMembers = Boolean(owner && hasScalarMembers(owner, listed))
const showMemberGroups = !scalarMembers && memberGroups.length > 1
const membersLabel = owner?.variant || (language === 'julia' && owner?.kind === 'struct' && listed.every((member) => member.kind === 'property')) ? 'Fields' : 'Members'
const pageSections: ApiSection[] = owner ? [
const glossaryContext = apiGlossary(`${refBase}glossary/`)
const pageSections: ApiSection[] = glossary ? apiGlossaryHeadings(glossaryContext.path).map((heading) => ({
id: heading.slug, label: heading.text, nested: heading.depth === 3,
})) : owner ? [
{ id: 'api-overview', label: 'Overview' },
...apiEntrySections(owner, language),
...(listed.length ? [{ id: 'api-members', label: membersLabel, count: listed.length },
Expand All @@ -372,7 +375,6 @@ const pageTitle = glossary ? API_GLOSSARY_TITLE : owner ? title : source?.title
const displayName = owner ? (declaringType ? `${declaringType.name}.${owner.name}` : owner.name) : pageTitle
const breadcrumbs = source?.breadcrumbs ?? await pageBreadcrumbs(displayName, glossary ? 'reference/glossary' : slug ? `reference/${slug}` : 'reference', version, port)
const pagePath = glossary ? 'reference/glossary' : source?.pagePath ?? (slug ? `reference/${slug}` : 'reference')
const glossaryContext = apiGlossary(`${refBase}glossary/`)
const description = glossary ? glossaryContext.description : source?.description ?? `Types, methods, and functions in libtmux for ${label}.`
const bases = owner && 'extends' in owner ? owner.extends ?? [] : []
---
Expand Down Expand Up @@ -417,7 +419,7 @@ const bases = owner && 'extends' in owner ? owner.extends ?? [] : []
</DocumentationContext>}
<!-- Flush: the tree's column runs from the header to the footer against the
viewport's edge, as Learn's does, and only the text column is padded. -->
<main class:list={["api-reference-layout grid", owner && 'api-reference-layout--symbol']}>
<main class:list={["api-reference-layout grid", owner && 'api-reference-layout--symbol', glossary && 'api-reference-layout--glossary']}>
{model && (
<ApiTree
referenceBase={refBase}
Expand Down Expand Up @@ -467,7 +469,17 @@ const bases = owner && 'extends' in owner ? owner.extends ?? [] : []
</>}
</header>

{glossary && <div class="prose dark:prose-invert mt-6 max-w-none"><ApiGlossaryContent contextPath={glossaryContext.path} /></div>}
{glossary && <>
<details class="api-glossary-contents" data-pagefind-ignore="all">
<summary>On this page</summary>
<nav aria-label="On this page"><ul>{pageSections.map((section) =>
<li class:list={[section.nested && 'api-contents-nested']}>
<a href={`#${section.id}`} data-api-section-link>{section.label}</a>
</li>)}
</ul></nav>
</details>
<div class="prose dark:prose-invert mt-6 max-w-none"><ApiGlossaryContent contextPath={glossaryContext.path} /></div>
</>}
{
!glossary && !owner && !model && (
<>
Expand Down Expand Up @@ -832,7 +844,7 @@ const bases = owner && 'extends' in owner ? owner.extends ?? [] : []
)
}
</div>
{owner && <ApiPageContents sections={pageSections} related={related} />}
{(owner || glossary) && <ApiPageContents sections={pageSections} related={related} />}
</main>


Expand Down
8 changes: 5 additions & 3 deletions site/src/components/docs/DocumentationContext.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ interface Props {
sourcePath: string
localeStatuses?: Record<string, LocaleStatus>
reference?: boolean
tableOfContents?: boolean
}
const { port, version, locale, sourcePath, localeStatuses, reference = false } = Astro.props
const { port, version, locale, sourcePath, localeStatuses, reference = false, tableOfContents = false } = Astro.props
const surfaces = await getDocumentationSurfaces(port, version, locale)
const command = sourcePath.match(/^tmux\/[^/]+\/manual\/(.+?)\/?$/)?.[1]
const sourceReference = sourcePath.match(/^tmux\/([^/]+)\/reference(?:\/(.*?))?\/?$/)
Expand All @@ -27,7 +28,7 @@ const tmuxVersions = !port ? TMUX_VERSIONS.map((entry) => ({
current: entry === version,
})) : undefined
---
<div class:list={['documentation-context', reference && 'documentation-context--reference']} data-documentation-context data-pagefind-ignore="all">
<div class:list={['documentation-context', reference && 'documentation-context--reference', tableOfContents && 'documentation-context--contents']} data-documentation-context data-pagefind-ignore="all">
<div class="documentation-context-inner">
<div class="documentation-context-surface">
<SurfacePicker surfaces={surfaces} currentPath={Astro.url.pathname} port={port} version={version} locale={locale} />
Expand Down Expand Up @@ -67,7 +68,8 @@ const tmuxVersions = !port ? TMUX_VERSIONS.map((entry) => ({
.documentation-context-navigation { display: flex; flex: none; align-items: center; gap: .4rem; margin-left: auto; padding: .25rem .75rem .25rem 0; }
.documentation-context-navigation:empty { display: none; }
.documentation-context-navigation :global(.api-nav-toggle) { flex: none; justify-content: center; width: 2.25rem; height: 2.25rem; margin: 0; padding: 0; gap: 0; }
@media (min-width: 64rem) { .documentation-context-navigation { display: none; } }
@media (min-width: 64rem) { .documentation-context:not(.documentation-context--contents) .documentation-context-navigation { display: none; } }
@media (min-width: 80rem) { .documentation-context--contents .documentation-context-navigation { display: none; } }
@media (min-width: 56rem) { .documentation-context--reference .documentation-context-navigation { display: none; } }
.documentation-context :global(.doc-picker) { position: static; }
.documentation-context :global(.doc-picker-panel) { left: .5rem; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Control = native ? 'label' : 'button'
aria-label="Open navigation menu"
aria-controls={native ? 'native-navigation' : 'mobile-sidebar'}
aria-expanded="false"
class="toolbar-button"
class:list={['toolbar-button', !native && 'documentation-sidebar-toggle']}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
Expand All @@ -40,4 +40,5 @@ const Control = native ? 'label' : 'button'
.toolbar-button { display: flex; align-items: center; justify-content: center; flex: none; height: 2.25rem; width: 2.25rem; border-radius: .375rem; border: 1px solid var(--color-edge); }
.toolbar-button:hover { background: var(--color-surface-hover); }
.toolbar-button:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; }
@media (min-width: 64rem) { .documentation-sidebar-toggle { display: none; } }
</style>
11 changes: 7 additions & 4 deletions site/src/components/widgets/PackageInstall.astro
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,12 @@ const cooldownGroup = (slug: string) => `cooldown-mode-${packageId}-${slug}`
}

/**
* Arrow keys along a tab strip.
* Keyboard navigation along a tab strip.
*
* `role="tablist"` is a promise that Left and Right move between the tabs;
* without it a keyboard reader Tabs through every tab in the strip to reach
* the content, which is what the roles exist to avoid. Shared by both strips
* the content, which is what the roles exist to avoid. Home and End select
* the first and last tabs. Shared by both strips
* because the mechanism is the same either way — only the selector and what
* a selection means differ.
*/
Expand All @@ -397,9 +398,11 @@ const cooldownGroup = (slug: string) => `cooldown-mode-${packageId}-${slug}`
select: (tab: HTMLButtonElement) => void,
) {
const delta = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0
if (!delta || tabs.length < 2) return
if ((!delta && event.key !== 'Home' && event.key !== 'End') || tabs.length < 2) return
event.preventDefault()
const next = tabs[(tabs.indexOf(current) + delta + tabs.length) % tabs.length]!
const index = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1
: (tabs.indexOf(current) + delta + tabs.length) % tabs.length
const next = tabs[index]!
select(next)
next.focus()
}
Expand Down
Loading
Loading