Skip to content

Commit

Permalink
feat: Correctly substitute documentation links for router-based navig…
Browse files Browse the repository at this point in the history
…ation

Some `href` attributes of the documentation links point to the static
FastAPI route, e.g. `http://localhost:3000/static/projects/name/version/`.
Since we want to use tanstack router for navigation, the links need to be
substituted with the UI navigation schema, in this case
`http://localhost:3000/name/version`.

This way, links from the documentation can be copied and shared.
  • Loading branch information
g3n35i5 committed Jan 31, 2025
1 parent e8cb917 commit 280ca54
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 14 deletions.
51 changes: 45 additions & 6 deletions src/ui/__tests__/helpers/routeHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,89 @@ describe('sanitizeDocuUri', () => {
const basePath = 'http://localhost:8080'
const testData = [
{
input: `${basePath}/project-one/6.0/index.html#`,
input: {
href: `${basePath}/project-one/6.0/index.html#`,
projectName: 'project-one',
projectVersion: '6.0',
},
expected: {
isInternal: true,
project: 'project-one',
remainder: 'index.html#',
version: '6.0',
href: `${basePath}/project-one/6.0/index.html#`,
},
},
{
input: `${basePath}/meta-project/1.3.0/#`,
input: {
href: `${basePath}/meta-project/1.3.0/#`,
projectName: 'meta-project',
projectVersion: '1.3.0',
},
expected: {
isInternal: true,
project: 'meta-project',
remainder: '#',
version: '1.3.0',
href: `${basePath}/meta-project/1.3.0/#`,
},
},
{
input: `${basePath}/meta-project/1.3.0`,
input: {
href: `${basePath}/meta-project/1.3.0`,
projectName: 'meta-project',
projectVersion: '1.3.0',
},
expected: {
isInternal: true,
project: 'meta-project',
remainder: undefined,
version: '1.3.0',
href: `${basePath}/meta-project/1.3.0`,
},
},
{
input: {
href: `${basePath}/project-one/latest/examples.html#examples`,
projectName: 'project-one',
projectVersion: 'latest',
},
expected: {
isInternal: true,
project: 'project-one',
remainder: 'examples.html#examples',
version: 'latest',
href: `${basePath}/project-one/latest/examples.html#examples`,
},
},
{
input: `${basePath}/project-one/latest/examples.html#examples`,
input: {
href: `${basePath}/static/projects/project-one/latest/examples.html#examples`,
projectName: 'project-one',
projectVersion: 'latest',
},
expected: {
isInternal: true,
project: 'project-one',
remainder: 'examples.html#examples',
version: 'latest',
href: `${basePath}/project-one/latest/examples.html#examples`,
},
},
{
input: 'https://www.sphinx-doc.org/',
input: {
href: 'https://www.sphinx-doc.org/',
projectName: 'example-project',
projectVersion: "doesn't matter",
},
expected: {
isInternal: false,
href: 'https://www.sphinx-doc.org/',
},
},
]
testData.forEach(({ input, expected }) => {
expect(sanitizeDocuUri(input, basePath)).toStrictEqual(expected)
expect(sanitizeDocuUri(input.href, basePath, input.projectName, input.projectVersion)).toStrictEqual(expected)
})
})
})
16 changes: 12 additions & 4 deletions src/ui/helpers/RouteHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ export interface SanitizeDocUriResI {
version?: string
remainder?: string
isInternal: boolean
href: string
}
export const sanitizeDocuUri = (href: string, basePath: string): SanitizeDocUriResI => {
export const sanitizeDocuUri = (
href: string,
basePath: string,
projectName: string,
projectVersion: string
): SanitizeDocUriResI => {
const escapeRegExp = (str: string): string => {
return str.replace(/[.*+?^=!:${}()|[\]/\\]/g, '\\$&')
}
Expand All @@ -17,10 +23,12 @@ export const sanitizeDocuUri = (href: string, basePath: string): SanitizeDocUriR
const match = href.match(re)
if (match && match.groups) {
const { name, version, remainder } = match.groups
return { project: name, version, remainder, isInternal: true }
const url = new URL(`${name}/${version}${remainder ? `/${remainder}` : ''}`, basePath)
return { project: name, version, remainder, isInternal: true, href: url.href }
} else if (!href.startsWith('http')) {
return { remainder: href, isInternal: true }
const url = new URL(`${projectName}/${projectVersion}/${href}`, basePath)
return { remainder: href, isInternal: true, href: url.href }
} else {
return { isInternal: false }
return { isInternal: false, href: href }
}
}
11 changes: 7 additions & 4 deletions src/ui/routes/$projectName/$version.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const fetchProjectDetailsAndSetStore = async (projectName: string, version: stri
latestVersion,
}))

return [version === 'latest' ? latestVersion : version, latestVersion]
return [version, latestVersion]
}

function DocumentationComponent() {
Expand Down Expand Up @@ -112,12 +112,13 @@ function DocuIFrame({ name, version, latestVersion, splat }: DocuIFramePropsI) {
anchorElements.forEach((anchor) => {
const href = anchor.getAttribute('href')
if (href) {
const result = sanitizeDocuUri(href, currentOrigin)
const result = sanitizeDocuUri(href, currentOrigin, name, version)
const params = {
projectName: result.project ?? name,
version: result.version ?? version,
_splat: result.remainder,
}
anchor.href = result.href
anchor.addEventListener('click', (event) => {
if (result.isInternal) {
event.preventDefault()
Expand All @@ -137,13 +138,15 @@ function DocuIFrame({ name, version, latestVersion, splat }: DocuIFramePropsI) {
}, [router, loaded, name, version])
return (
<Fragment>
{name && version !== latestVersion && <DeprecatedVersionBanner name={name} version={version} />}
{name && version !== 'latest' && version !== latestVersion && (
<DeprecatedVersionBanner name={name} version={version} />
)}
<iframe
data-testid={'docIframe'}
ref={iframeRef}
onLoad={() => setLoaded(true)}
style={{ border: 0, width: '100%', height: '100%' }}
src={`/static/projects/${name}/${version}/${splat}${window.location.hash}`}
src={`/static/projects/${name}/${version === 'latest' ? latestVersion : version}/${splat}${window.location.hash}`}
/>
</Fragment>
)
Expand Down
5 changes: 5 additions & 0 deletions tests/ui/resources/mockedIndex.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@
</style>
<body>
<h1>Hello, this is a mocked documentation component.</h1>
<ul>
<li><a href="#">Link with href '#'</a></li>
<li><a href="index.html">Link with href 'index.html'</a></li>
<li><a href="https://www.sphinx-doc.org/">Link with href 'https://www.sphinx-doc.org/'</a></li>
</ul>
</body>
</html>
24 changes: 24 additions & 0 deletions tests/ui/testLinkSubstitution.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from '@playwright/test'
import test from './base'

test('Test link substitution', async ({ page }) => {
const docIframe = page.getByTestId('docIframe')
const expectedDocumentationContent = 'Hello, this is a mocked documentation component.'
const basePath = 'http://localhost:3000'

// Expect the documentation iframe to display the mocked documentation page
await page.goto('/example-project-01/latest')
await expect(docIframe.contentFrame().locator('html')).toContainText(expectedDocumentationContent)

// Ensure that all links have been substituted correctly
const links = docIframe.contentFrame().getByRole('link')
await expect(links).toHaveCount(3)
const expectedSubstitutedLinks = [
`${basePath}/example-project-01/latest/#`,
`${basePath}/example-project-01/latest/index.html`,
'https://www.sphinx-doc.org/',
]
for (const [index, expectedLink] of expectedSubstitutedLinks.entries()) {
expect(await links.nth(index).getAttribute('href')).toBe(expectedLink)
}
})

0 comments on commit 280ca54

Please sign in to comment.