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' ;
2
11
import 'katex/dist/katex.min.css' ;
3
12
import React , { ClassAttributes , FunctionComponent , HTMLAttributes , useState } from 'react' ;
4
13
import { FaChevronDown , FaChevronRight } from 'react-icons/fa' ;
5
14
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' ;
7
22
import rehypeKatex from 'rehype-katex' ;
8
23
import remarkGfm from 'remark-gfm' ;
9
24
import remarkMath from 'remark-math' ;
10
25
import { useAppSelector } from '../../../app/hooks' ;
11
26
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' ;
12
30
13
31
interface DocumentationTextProps {
32
+ declaration : PythonDeclaration ;
14
33
inputText : string ;
15
34
}
16
35
17
36
type ParagraphComponent = FunctionComponent <
18
37
ClassAttributes < HTMLParagraphElement > & HTMLAttributes < HTMLParagraphElement > & ReactMarkdownProps
19
38
> ;
20
39
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
+ ) ;
23
48
} ;
24
49
25
50
const CustomCode : CodeComponent = function ( { className, children } ) {
26
51
return < Code className = { className } > { children } </ Code > ;
27
52
} ;
28
53
54
+ const CustomText : ParagraphComponent = function ( { className, children } ) {
55
+ return < ChakraText className = { className } > { children } </ ChakraText > ;
56
+ } ;
57
+
29
58
const CustomUnorderedList : UnorderedListComponent = function ( { className, children } ) {
30
59
return < UnorderedList className = { className } > { children } </ UnorderedList > ;
31
60
} ;
32
61
33
62
const components = {
34
- p : CustomText ,
63
+ a : CustomLink ,
35
64
code : CustomCode ,
65
+ p : CustomText ,
36
66
ul : CustomUnorderedList ,
37
67
} ;
38
68
39
- export const DocumentationText : React . FC < DocumentationTextProps > = function ( { inputText = '' } ) {
69
+ export const DocumentationText : React . FC < DocumentationTextProps > = function ( { declaration , inputText = '' } ) {
40
70
const expandDocumentationByDefault = useAppSelector ( selectExpandDocumentationByDefault ) ;
41
71
42
72
const preprocessedText = inputText
@@ -45,7 +75,21 @@ export const DocumentationText: React.FC<DocumentationTextProps> = function ({ i
45
75
// replace inline math elements
46
76
. replaceAll ( / : m a t h : ` ( [ ^ ` ] * ) ` / gu, '$$1$' )
47
77
// replace block math elements
48
- . replaceAll ( / \. \. m a t h : : \s * ( \S .* ) \n \n / gu, '$$\n$1\n$$\n\n' ) ;
78
+ . replaceAll ( / \. \. m a t h : : \s * ( \S .* ) \n \n / gu, '$$\n$1\n$$\n\n' )
79
+ // replace relative links to classes
80
+ . replaceAll ( / : c l a s s : ` ( \w * ) ` / gu, ( _match , name ) => resolveRelativeLink ( declaration , name ) )
81
+ // replace relative links to functions
82
+ . replaceAll ( / : f u n c : ` ( \w * ) ` / gu, ( _match , name ) => resolveRelativeLink ( declaration , name ) )
83
+ // replace absolute links to modules
84
+ . replaceAll ( / : m o d : ` ( [ \w . ] * ) ` / gu, ( _match , qualifiedName ) => resolveAbsoluteLink ( declaration , qualifiedName , 1 ) )
85
+ // replace absolute links to classes
86
+ . replaceAll ( / : c l a s s : ` ~ ? ( [ \w . ] * ) ` / gu, ( _match , qualifiedName ) =>
87
+ resolveAbsoluteLink ( declaration , qualifiedName , 2 ) ,
88
+ )
89
+ // replace absolute links to classes
90
+ . replaceAll ( / : f u n c : ` ~ ? ( [ \w . ] * ) ` / gu, ( _match , qualifiedName ) =>
91
+ resolveAbsoluteLink ( declaration , qualifiedName , 2 ) ,
92
+ ) ;
49
93
50
94
const shortenedText = preprocessedText . split ( '\n\n' ) [ 0 ] ;
51
95
const hasMultipleLines = shortenedText !== preprocessedText ;
@@ -91,3 +135,49 @@ export const DocumentationText: React.FC<DocumentationTextProps> = function ({ i
91
135
</ Flex >
92
136
) ;
93
137
} ;
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
+ } ;
0 commit comments