Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit 6cda741

Browse files
authored
feat(gui): links to other declarations in documentation (#797)
* feat(gui): resolve relative links to functions in documentation * feat(gui): resolve relative links to classes in documentation * feat(gui): underline links in documentation * feat(gui): replace links to modules * feat(gui): always display preferred qualified name
1 parent 165a8da commit 6cda741

File tree

6 files changed

+112
-11
lines changed

6 files changed

+112
-11
lines changed

api-editor/gui/src/features/menuBar/SelectionBreadcrumbs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const SelectionBreadcrumbs = function () {
1818
return (
1919
<Breadcrumb>
2020
{declarations.map((it) => (
21-
<BreadcrumbItem>
21+
<BreadcrumbItem key={it.id}>
2222
<RouterLink to={it.id}>{it.name}</RouterLink>
2323
</BreadcrumbItem>
2424
))}

api-editor/gui/src/features/packageData/model/PythonDeclaration.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ export abstract class PythonDeclaration {
1515
return this.name;
1616
}
1717

18+
root(): PythonDeclaration {
19+
let current: PythonDeclaration = this;
20+
while (true) {
21+
const parent = current.parent();
22+
if (!parent) {
23+
return current;
24+
}
25+
current = parent;
26+
}
27+
}
28+
1829
*ancestorsOrSelf(): Generator<PythonDeclaration> {
1930
let current: Optional<PythonDeclaration> = this;
2031
while (current) {

api-editor/gui/src/features/packageData/selectionView/ClassView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const ClassView: React.FC<ClassViewProps> = function ({ pythonClass }) {
3333

3434
<Box paddingLeft={4}>
3535
{pythonClass.description ? (
36-
<DocumentationText inputText={pythonClass.description} />
36+
<DocumentationText declaration={pythonClass} inputText={pythonClass.description} />
3737
) : (
3838
<ChakraText color="gray.500">There is no documentation for this class.</ChakraText>
3939
)}

api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,72 @@
1-
import { Code, Flex, HStack, IconButton, Stack, Text as ChakraText, UnorderedList } from '@chakra-ui/react';
1+
import {
2+
Code,
3+
Flex,
4+
HStack,
5+
IconButton,
6+
Link as ChakraLink,
7+
Stack,
8+
Text as ChakraText,
9+
UnorderedList,
10+
} from '@chakra-ui/react';
211
import 'katex/dist/katex.min.css';
312
import React, { ClassAttributes, FunctionComponent, HTMLAttributes, useState } from 'react';
413
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
514
import ReactMarkdown from 'react-markdown';
6-
import { CodeComponent, ReactMarkdownProps, UnorderedListComponent } from 'react-markdown/lib/ast-to-react';
15+
import {
16+
CodeComponent,
17+
ComponentPropsWithoutRef,
18+
ComponentType,
19+
ReactMarkdownProps,
20+
UnorderedListComponent,
21+
} from 'react-markdown/lib/ast-to-react';
722
import rehypeKatex from 'rehype-katex';
823
import remarkGfm from 'remark-gfm';
924
import remarkMath from 'remark-math';
1025
import { useAppSelector } from '../../../app/hooks';
1126
import { selectExpandDocumentationByDefault } from '../../ui/uiSlice';
27+
import { Link as RouterLink } from 'react-router-dom';
28+
import { PythonDeclaration } from '../model/PythonDeclaration';
29+
import { PythonPackage } from '../model/PythonPackage';
1230

1331
interface DocumentationTextProps {
32+
declaration: PythonDeclaration;
1433
inputText: string;
1534
}
1635

1736
type ParagraphComponent = FunctionComponent<
1837
ClassAttributes<HTMLParagraphElement> & HTMLAttributes<HTMLParagraphElement> & ReactMarkdownProps
1938
>;
2039

21-
const CustomText: ParagraphComponent = function ({ className, children }) {
22-
return <ChakraText className={className}>{children}</ChakraText>;
40+
type LinkComponent = ComponentType<ComponentPropsWithoutRef<'a'> & ReactMarkdownProps>;
41+
42+
const CustomLink: LinkComponent = function ({ className, children, href }) {
43+
return (
44+
<ChakraLink as={RouterLink} to={href ?? '#'} className={className} textDecoration="underline">
45+
{children}
46+
</ChakraLink>
47+
);
2348
};
2449

2550
const CustomCode: CodeComponent = function ({ className, children }) {
2651
return <Code className={className}>{children}</Code>;
2752
};
2853

54+
const CustomText: ParagraphComponent = function ({ className, children }) {
55+
return <ChakraText className={className}>{children}</ChakraText>;
56+
};
57+
2958
const CustomUnorderedList: UnorderedListComponent = function ({ className, children }) {
3059
return <UnorderedList className={className}>{children}</UnorderedList>;
3160
};
3261

3362
const components = {
34-
p: CustomText,
63+
a: CustomLink,
3564
code: CustomCode,
65+
p: CustomText,
3666
ul: CustomUnorderedList,
3767
};
3868

39-
export const DocumentationText: React.FC<DocumentationTextProps> = function ({ inputText = '' }) {
69+
export const DocumentationText: React.FC<DocumentationTextProps> = function ({ declaration, inputText = '' }) {
4070
const expandDocumentationByDefault = useAppSelector(selectExpandDocumentationByDefault);
4171

4272
const preprocessedText = inputText
@@ -45,7 +75,21 @@ export const DocumentationText: React.FC<DocumentationTextProps> = function ({ i
4575
// replace inline math elements
4676
.replaceAll(/:math:`([^`]*)`/gu, '$$1$')
4777
// replace block math elements
48-
.replaceAll(/\.\. math::\s*(\S.*)\n\n/gu, '$$\n$1\n$$\n\n');
78+
.replaceAll(/\.\. math::\s*(\S.*)\n\n/gu, '$$\n$1\n$$\n\n')
79+
// replace relative links to classes
80+
.replaceAll(/:class:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name))
81+
// replace relative links to functions
82+
.replaceAll(/:func:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name))
83+
// replace absolute links to modules
84+
.replaceAll(/:mod:`([\w.]*)`/gu, (_match, qualifiedName) => resolveAbsoluteLink(declaration, qualifiedName, 1))
85+
// replace absolute links to classes
86+
.replaceAll(/:class:`~?([\w.]*)`/gu, (_match, qualifiedName) =>
87+
resolveAbsoluteLink(declaration, qualifiedName, 2),
88+
)
89+
// replace absolute links to classes
90+
.replaceAll(/:func:`~?([\w.]*)`/gu, (_match, qualifiedName) =>
91+
resolveAbsoluteLink(declaration, qualifiedName, 2),
92+
);
4993

5094
const shortenedText = preprocessedText.split('\n\n')[0];
5195
const hasMultipleLines = shortenedText !== preprocessedText;
@@ -91,3 +135,49 @@ export const DocumentationText: React.FC<DocumentationTextProps> = function ({ i
91135
</Flex>
92136
);
93137
};
138+
139+
const resolveRelativeLink = function (currentDeclaration: PythonDeclaration, linkedDeclarationName: string): string {
140+
const parent = currentDeclaration.parent();
141+
if (!parent) {
142+
return linkedDeclarationName;
143+
}
144+
145+
const sibling = parent.children().find((it) => it.name === linkedDeclarationName);
146+
if (!sibling) {
147+
return linkedDeclarationName;
148+
}
149+
150+
return `[${currentDeclaration.preferredQualifiedName()}](${sibling.id})`;
151+
};
152+
153+
const resolveAbsoluteLink = function (
154+
currentDeclaration: PythonDeclaration,
155+
linkedDeclarationQualifiedName: string,
156+
segmentCount: number,
157+
): string {
158+
let segments = linkedDeclarationQualifiedName.split('.');
159+
if (segments.length < segmentCount) {
160+
return linkedDeclarationQualifiedName;
161+
}
162+
163+
segments = [
164+
segments.slice(0, segments.length - segmentCount + 1).join('.'),
165+
...segments.slice(segments.length - segmentCount + 1),
166+
];
167+
168+
let current = currentDeclaration.root();
169+
if (!(current instanceof PythonPackage)) {
170+
return linkedDeclarationQualifiedName;
171+
}
172+
173+
for (const segment of segments) {
174+
const next = current.children().find((it) => it.name === segment);
175+
if (!next) {
176+
return linkedDeclarationQualifiedName;
177+
}
178+
179+
current = next;
180+
}
181+
182+
return `[${current.preferredQualifiedName()}](${current.id})`;
183+
};

api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const FunctionView: React.FC<FunctionViewProps> = function ({ pythonFunct
5757

5858
<Box paddingLeft={4}>
5959
{pythonFunction.description ? (
60-
<DocumentationText inputText={pythonFunction.description} />
60+
<DocumentationText declaration={pythonFunction} inputText={pythonFunction.description} />
6161
) : (
6262
<ChakraText color="gray.500">There is no documentation for this function.</ChakraText>
6363
)}

api-editor/gui/src/features/packageData/selectionView/ParameterNode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const ParameterNode: React.FC<ParameterNodeProps> = function ({ isTitle,
5555

5656
<Box paddingLeft={4}>
5757
{pythonParameter.description ? (
58-
<DocumentationText inputText={pythonParameter?.description} />
58+
<DocumentationText declaration={pythonParameter} inputText={pythonParameter?.description} />
5959
) : (
6060
<ChakraText color="gray.500">There is no documentation for this parameter.</ChakraText>
6161
)}

0 commit comments

Comments
 (0)