From 4feaab9ae9d74c6a59c173d5922317fb5f8648b3 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 25 Jun 2022 21:21:40 +0200 Subject: [PATCH 1/5] feat(gui): resolve relative links to functions in documentation --- .../packageData/selectionView/ClassView.tsx | 2 +- .../selectionView/DocumentationText.tsx | 59 ++++++++++++++++--- .../selectionView/FunctionView.tsx | 2 +- .../selectionView/ParameterNode.tsx | 2 +- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx b/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx index c1df2babb..5315c4e94 100644 --- a/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/ClassView.tsx @@ -33,7 +33,7 @@ export const ClassView: React.FC = function ({ pythonClass }) { {pythonClass.description ? ( - + ) : ( There is no documentation for this class. )} diff --git a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx index 9790ea0ca..33f2f9fc7 100644 --- a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx @@ -1,16 +1,34 @@ -import { Code, Flex, HStack, IconButton, Stack, Text as ChakraText, UnorderedList } from '@chakra-ui/react'; +import { + Code, + Flex, + HStack, + IconButton, + Link as ChakraLink, + Stack, + Text as ChakraText, + UnorderedList, +} from '@chakra-ui/react'; import 'katex/dist/katex.min.css'; import React, { ClassAttributes, FunctionComponent, HTMLAttributes, useState } from 'react'; import { FaChevronDown, FaChevronRight } from 'react-icons/fa'; import ReactMarkdown from 'react-markdown'; -import { CodeComponent, ReactMarkdownProps, UnorderedListComponent } from 'react-markdown/lib/ast-to-react'; +import { + CodeComponent, + ComponentPropsWithoutRef, + ComponentType, + ReactMarkdownProps, + UnorderedListComponent, +} from 'react-markdown/lib/ast-to-react'; import rehypeKatex from 'rehype-katex'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import { useAppSelector } from '../../../app/hooks'; import { selectExpandDocumentationByDefault } from '../../ui/uiSlice'; +import { Link as RouterLink } from 'react-router-dom'; +import { PythonDeclaration } from '../model/PythonDeclaration'; interface DocumentationTextProps { + declaration: PythonDeclaration; inputText: string; } @@ -18,25 +36,36 @@ type ParagraphComponent = FunctionComponent< ClassAttributes & HTMLAttributes & ReactMarkdownProps >; -const CustomText: ParagraphComponent = function ({ className, children }) { - return {children}; +type LinkComponent = ComponentType & ReactMarkdownProps>; + +const CustomLink: LinkComponent = function ({ className, children, href }) { + return ( + + {children} + + ); }; const CustomCode: CodeComponent = function ({ className, children }) { return {children}; }; +const CustomText: ParagraphComponent = function ({ className, children }) { + return {children}; +}; + const CustomUnorderedList: UnorderedListComponent = function ({ className, children }) { return {children}; }; const components = { - p: CustomText, + a: CustomLink, code: CustomCode, + p: CustomText, ul: CustomUnorderedList, }; -export const DocumentationText: React.FC = function ({ inputText = '' }) { +export const DocumentationText: React.FC = function ({ declaration, inputText = '' }) { const expandDocumentationByDefault = useAppSelector(selectExpandDocumentationByDefault); const preprocessedText = inputText @@ -45,7 +74,9 @@ export const DocumentationText: React.FC = function ({ i // replace inline math elements .replaceAll(/:math:`([^`]*)`/gu, '$$1$') // replace block math elements - .replaceAll(/\.\. math::\s*(\S.*)\n\n/gu, '$$\n$1\n$$\n\n'); + .replaceAll(/\.\. math::\s*(\S.*)\n\n/gu, '$$\n$1\n$$\n\n') + // replace relative links to functions + .replaceAll(/:func:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)); const shortenedText = preprocessedText.split('\n\n')[0]; const hasMultipleLines = shortenedText !== preprocessedText; @@ -91,3 +122,17 @@ export const DocumentationText: React.FC = function ({ i ); }; + +const resolveRelativeLink = function (currentDeclaration: PythonDeclaration, linkedDeclarationName: string): string { + const parent = currentDeclaration.parent(); + if (!parent) { + return linkedDeclarationName; + } + + const sibling = parent.children().find(it => it.name === linkedDeclarationName); + if (!sibling) { + return linkedDeclarationName; + } + + return `[${linkedDeclarationName}](${sibling.id})`; +}; diff --git a/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx b/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx index 7ccb32baf..f148eee89 100644 --- a/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/FunctionView.tsx @@ -57,7 +57,7 @@ export const FunctionView: React.FC = function ({ pythonFunct {pythonFunction.description ? ( - + ) : ( There is no documentation for this function. )} diff --git a/api-editor/gui/src/features/packageData/selectionView/ParameterNode.tsx b/api-editor/gui/src/features/packageData/selectionView/ParameterNode.tsx index bbd2f15c4..9051299f2 100644 --- a/api-editor/gui/src/features/packageData/selectionView/ParameterNode.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/ParameterNode.tsx @@ -55,7 +55,7 @@ export const ParameterNode: React.FC = function ({ isTitle, {pythonParameter.description ? ( - + ) : ( There is no documentation for this parameter. )} From 2dad600d99a5b9f967a5148ffb53af7e3607c2b9 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 25 Jun 2022 21:24:27 +0200 Subject: [PATCH 2/5] feat(gui): resolve relative links to classes in documentation --- .../features/packageData/selectionView/DocumentationText.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx index 33f2f9fc7..b09470e15 100644 --- a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx @@ -75,6 +75,8 @@ export const DocumentationText: React.FC = function ({ d .replaceAll(/:math:`([^`]*)`/gu, '$$1$') // replace block math elements .replaceAll(/\.\. math::\s*(\S.*)\n\n/gu, '$$\n$1\n$$\n\n') + // replace relative links to classes + .replaceAll(/:class:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)) // replace relative links to functions .replaceAll(/:func:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)); @@ -129,7 +131,7 @@ const resolveRelativeLink = function (currentDeclaration: PythonDeclaration, lin return linkedDeclarationName; } - const sibling = parent.children().find(it => it.name === linkedDeclarationName); + const sibling = parent.children().find((it) => it.name === linkedDeclarationName); if (!sibling) { return linkedDeclarationName; } From 2d335c83700b26e42a9399e715177810c0ff6b9b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 25 Jun 2022 21:26:46 +0200 Subject: [PATCH 3/5] feat(gui): underline links in documentation --- .../features/packageData/selectionView/DocumentationText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx index b09470e15..7612fc6e6 100644 --- a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx @@ -40,7 +40,7 @@ type LinkComponent = ComponentType & ReactMarkdown const CustomLink: LinkComponent = function ({ className, children, href }) { return ( - + {children} ); From c8740bdde1855fd36ef659e231a1f72b9b812c4e Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 25 Jun 2022 21:47:13 +0200 Subject: [PATCH 4/5] feat(gui): replace links to modules --- .../features/menuBar/SelectionBreadcrumbs.tsx | 2 +- .../packageData/model/PythonDeclaration.ts | 11 ++++++ .../selectionView/DocumentationText.tsx | 37 ++++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/api-editor/gui/src/features/menuBar/SelectionBreadcrumbs.tsx b/api-editor/gui/src/features/menuBar/SelectionBreadcrumbs.tsx index a7093c34f..a29cefcd2 100644 --- a/api-editor/gui/src/features/menuBar/SelectionBreadcrumbs.tsx +++ b/api-editor/gui/src/features/menuBar/SelectionBreadcrumbs.tsx @@ -18,7 +18,7 @@ export const SelectionBreadcrumbs = function () { return ( {declarations.map((it) => ( - + {it.name} ))} diff --git a/api-editor/gui/src/features/packageData/model/PythonDeclaration.ts b/api-editor/gui/src/features/packageData/model/PythonDeclaration.ts index f0f37e4d7..a34157cc1 100644 --- a/api-editor/gui/src/features/packageData/model/PythonDeclaration.ts +++ b/api-editor/gui/src/features/packageData/model/PythonDeclaration.ts @@ -15,6 +15,17 @@ export abstract class PythonDeclaration { return this.name; } + root(): PythonDeclaration { + let current: PythonDeclaration = this; + while (true) { + const parent = current.parent(); + if (!parent) { + return current; + } + current = parent; + } + } + *ancestorsOrSelf(): Generator { let current: Optional = this; while (current) { diff --git a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx index 7612fc6e6..b34266683 100644 --- a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx @@ -26,6 +26,7 @@ import { useAppSelector } from '../../../app/hooks'; import { selectExpandDocumentationByDefault } from '../../ui/uiSlice'; import { Link as RouterLink } from 'react-router-dom'; import { PythonDeclaration } from '../model/PythonDeclaration'; +import {PythonPackage} from "../model/PythonPackage"; interface DocumentationTextProps { declaration: PythonDeclaration; @@ -78,7 +79,9 @@ export const DocumentationText: React.FC = function ({ d // replace relative links to classes .replaceAll(/:class:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)) // replace relative links to functions - .replaceAll(/:func:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)); + .replaceAll(/:func:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)) + // replace absolute links to modules + .replaceAll(/:mod:`([\w.]*)`/gu, (_match, qualifiedName) => resolveAbsoluteLink(declaration, qualifiedName, 1)); const shortenedText = preprocessedText.split('\n\n')[0]; const hasMultipleLines = shortenedText !== preprocessedText; @@ -138,3 +141,35 @@ const resolveRelativeLink = function (currentDeclaration: PythonDeclaration, lin return `[${linkedDeclarationName}](${sibling.id})`; }; + +const resolveAbsoluteLink = function ( + currentDeclaration: PythonDeclaration, + linkedDeclarationQualifiedName: string, + segmentCount: number +): string { + let segments = linkedDeclarationQualifiedName.split('.'); + if (segments.length < segmentCount) { + return linkedDeclarationQualifiedName; + } + + segments = [ + segments.slice(0, segments.length - segmentCount + 1).join('.'), + ...segments.slice(segments.length - segmentCount + 1), + ] + + let current = currentDeclaration.root() + if (!(current instanceof PythonPackage)) { + return linkedDeclarationQualifiedName; + } + + for (const segment of segments) { + const next = current.children().find((it) => it.name === segment); + if (!next) { + return linkedDeclarationQualifiedName; + } + + current = next; + } + + return `[${linkedDeclarationQualifiedName}](${current.id})`; +}; From aebb422d6dcba64874d8a28e878febf9a2fe3300 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sat, 25 Jun 2022 22:00:49 +0200 Subject: [PATCH 5/5] feat(gui): always display preferred qualified name --- .../selectionView/DocumentationText.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx index b34266683..ca1cdb48c 100644 --- a/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx +++ b/api-editor/gui/src/features/packageData/selectionView/DocumentationText.tsx @@ -26,7 +26,7 @@ import { useAppSelector } from '../../../app/hooks'; import { selectExpandDocumentationByDefault } from '../../ui/uiSlice'; import { Link as RouterLink } from 'react-router-dom'; import { PythonDeclaration } from '../model/PythonDeclaration'; -import {PythonPackage} from "../model/PythonPackage"; +import { PythonPackage } from '../model/PythonPackage'; interface DocumentationTextProps { declaration: PythonDeclaration; @@ -81,7 +81,15 @@ export const DocumentationText: React.FC = function ({ d // replace relative links to functions .replaceAll(/:func:`(\w*)`/gu, (_match, name) => resolveRelativeLink(declaration, name)) // replace absolute links to modules - .replaceAll(/:mod:`([\w.]*)`/gu, (_match, qualifiedName) => resolveAbsoluteLink(declaration, qualifiedName, 1)); + .replaceAll(/:mod:`([\w.]*)`/gu, (_match, qualifiedName) => resolveAbsoluteLink(declaration, qualifiedName, 1)) + // replace absolute links to classes + .replaceAll(/:class:`~?([\w.]*)`/gu, (_match, qualifiedName) => + resolveAbsoluteLink(declaration, qualifiedName, 2), + ) + // replace absolute links to classes + .replaceAll(/:func:`~?([\w.]*)`/gu, (_match, qualifiedName) => + resolveAbsoluteLink(declaration, qualifiedName, 2), + ); const shortenedText = preprocessedText.split('\n\n')[0]; const hasMultipleLines = shortenedText !== preprocessedText; @@ -139,13 +147,13 @@ const resolveRelativeLink = function (currentDeclaration: PythonDeclaration, lin return linkedDeclarationName; } - return `[${linkedDeclarationName}](${sibling.id})`; + return `[${currentDeclaration.preferredQualifiedName()}](${sibling.id})`; }; const resolveAbsoluteLink = function ( currentDeclaration: PythonDeclaration, linkedDeclarationQualifiedName: string, - segmentCount: number + segmentCount: number, ): string { let segments = linkedDeclarationQualifiedName.split('.'); if (segments.length < segmentCount) { @@ -155,9 +163,9 @@ const resolveAbsoluteLink = function ( segments = [ segments.slice(0, segments.length - segmentCount + 1).join('.'), ...segments.slice(segments.length - segmentCount + 1), - ] + ]; - let current = currentDeclaration.root() + let current = currentDeclaration.root(); if (!(current instanceof PythonPackage)) { return linkedDeclarationQualifiedName; } @@ -171,5 +179,5 @@ const resolveAbsoluteLink = function ( current = next; } - return `[${linkedDeclarationQualifiedName}](${current.id})`; + return `[${current.preferredQualifiedName()}](${current.id})`; };