diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx
index 0ba0b47458..b55f42e984 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx
@@ -14,34 +14,33 @@
* limitations under the License.
*/
-import React, { ReactNode, useMemo } from 'react';
+import React, { ReactNode } from 'react';
import { useParams } from 'react-router-dom';
+import { Page } from '@backstage/core-components';
import { CompoundEntityRef } from '@backstage/catalog-model';
+import { TechDocsReaderPageRenderFunction } from '../../../types';
+
import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent';
import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader';
import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader';
-import { TechDocsReaderPageRenderFunction } from '../../../types';
-
-import {
- TechDocsEntityProvider,
- TechDocsMetadataProvider,
- TechDocsReaderPageProvider,
-} from './context';
+import { TechDocsReaderPageProvider } from './context';
export type TechDocsReaderLayoutProps = {
hideHeader?: boolean;
+ withSearch?: boolean;
};
export const TechDocsReaderLayout = ({
hideHeader = false,
+ withSearch,
}: TechDocsReaderLayoutProps) => (
<>
{!hideHeader && }
-
+
>
);
@@ -49,7 +48,6 @@ export const TechDocsReaderLayout = ({
* @public
*/
export type TechDocsReaderPageProps = {
- path?: string;
entityName?: CompoundEntityRef;
children?: TechDocsReaderPageRenderFunction | ReactNode;
};
@@ -59,37 +57,26 @@ export type TechDocsReaderPageProps = {
* @public
*/
export const TechDocsReaderPage = ({
- path: defaultPath,
entityName: defaultEntityName,
children = ,
}: TechDocsReaderPageProps) => {
- const params = useParams();
+ const { kind, name, namespace } = useParams();
- const path = useMemo(() => {
- if (defaultPath) {
- return defaultPath;
- }
- return params['*'];
- }, [params, defaultPath]);
-
- const entityName = useMemo(() => {
- if (defaultEntityName) {
- return defaultEntityName;
- }
- return {
- kind: params.kind,
- name: params.name,
- namespace: params.namespace,
- };
- }, [params, defaultEntityName]);
+ const entityName = defaultEntityName || { kind, name, namespace };
return (
-
-
-
- {children}
-
-
-
+
+ {({ metadata, entityMetadata }) => (
+
+ {children instanceof Function
+ ? children({
+ entityRef: entityName,
+ techdocsMetadataValue: metadata.value,
+ entityMetadataValue: entityMetadata.value,
+ })
+ : children}
+
+ )}
+
);
};
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx
index 56e0991df6..bd1c884ca0 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/context.tsx
@@ -15,92 +15,42 @@
*/
import React, {
- createContext,
+ ReactNode,
+ memo,
Dispatch,
- PropsWithChildren,
SetStateAction,
+ createContext,
useContext,
useState,
} from 'react';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
-import { Page } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { techdocsApiRef } from '../../../api';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../../types';
-type PropsWithEntityName = T &
- PropsWithChildren<{ entityName: CompoundEntityRef }>;
-
-const initialContextValue = {
- loading: true,
- error: undefined,
- value: undefined,
-};
-
-const TechDocsMetadataContext =
- createContext>(initialContextValue);
-
-export const TechDocsMetadataProvider = ({
- entityName,
- children,
-}: PropsWithEntityName) => {
- const techdocsApi = useApi(techdocsApiRef);
-
- const value = useAsync(async () => {
- return techdocsApi.getTechDocsMetadata(entityName);
- }, [entityName]);
-
- return (
-
- {children}
-
- );
-};
-
-/**
- * Hook for use within TechDocs addons to retrieve TechDocs Metadata for the
- * current TechDocs site.
- * @public
- */
-export const useTechDocsMetadata = () => {
- return useContext(TechDocsMetadataContext);
-};
-
-const TechDocsEntityContext =
- createContext>(initialContextValue);
-
-export const TechDocsEntityProvider = ({
- entityName,
- children,
-}: PropsWithEntityName) => {
- const techdocsApi = useApi(techdocsApiRef);
-
- const value = useAsync(async () => {
- return techdocsApi.getEntityMetadata(entityName);
- }, [entityName]);
-
- return (
-
- {children}
-
- );
-};
-
-/**
- * Hook for use within TechDocs addons to retrieve Entity Metadata for the
- * current TechDocs site.
- * @public
- */
-export const useEntityMetadata = () => {
- return useContext(TechDocsEntityContext);
+const areEntityNamesEqual = (
+ prevEntityName: CompoundEntityRef,
+ nextEntityName: CompoundEntityRef,
+) => {
+ if (prevEntityName.kind !== nextEntityName.kind) {
+ return false;
+ }
+ if (prevEntityName.name !== nextEntityName.name) {
+ return false;
+ }
+ if (prevEntityName.namespace !== nextEntityName.namespace) {
+ return false;
+ }
+ return true;
};
export type TechDocsReaderPageValue = {
- path: string;
+ metadata: AsyncState;
entityName: CompoundEntityRef;
+ entityMetadata: AsyncState;
shadowRoot?: ShadowRoot;
setShadowRoot: Dispatch>;
title: string;
@@ -110,12 +60,13 @@ export type TechDocsReaderPageValue = {
};
export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue = {
- path: '',
title: '',
- setTitle: () => {},
subtitle: '',
+ setTitle: () => {},
setSubtitle: () => {},
setShadowRoot: () => {},
+ metadata: { loading: true },
+ entityMetadata: { loading: true },
entityName: { kind: '', name: '', namespace: '' },
};
@@ -127,48 +78,74 @@ export const useTechDocsReaderPage = () => {
return useContext(TechDocsReaderPageContext);
};
-type TechDocsReaderPageProviderProps = PropsWithEntityName<{
- path?: string;
-}>;
+type TechDocsReaderPageProviderRenderFunction = (
+ value: TechDocsReaderPageValue,
+) => JSX.Element;
-export const TechDocsReaderPageProvider = ({
- path = '',
- entityName,
- children,
-}: TechDocsReaderPageProviderProps) => {
- const { value: entityMetadataValue } = useEntityMetadata();
- const { value: techdocsMetadataValue } = useTechDocsMetadata();
-
- const [title, setTitle] = useState(defaultTechDocsReaderPageValue.title);
- const [subtitle, setSubtitle] = useState(
- defaultTechDocsReaderPageValue.subtitle,
- );
- const [shadowRoot, setShadowRoot] = useState(
- defaultTechDocsReaderPageValue.shadowRoot,
- );
-
- const value = {
- path,
- entityName,
- shadowRoot,
- setShadowRoot,
- title,
- setTitle,
- subtitle,
- setSubtitle,
- };
-
- return (
-
-
- {children instanceof Function
- ? children({
- entityRef: entityName,
- entityMetadataValue,
- techdocsMetadataValue,
- })
- : children}
-
-
- );
+type TechDocsReaderPageProviderProps = {
+ entityName: CompoundEntityRef;
+ children: TechDocsReaderPageProviderRenderFunction | ReactNode;
+};
+
+export const TechDocsReaderPageProvider = memo(
+ ({ entityName, children }: TechDocsReaderPageProviderProps) => {
+ const techdocsApi = useApi(techdocsApiRef);
+
+ const metadata = useAsync(async () => {
+ return techdocsApi.getTechDocsMetadata(entityName);
+ }, [entityName]);
+
+ const entityMetadata = useAsync(async () => {
+ return techdocsApi.getEntityMetadata(entityName);
+ }, [entityName]);
+
+ const [title, setTitle] = useState(defaultTechDocsReaderPageValue.title);
+ const [subtitle, setSubtitle] = useState(
+ defaultTechDocsReaderPageValue.subtitle,
+ );
+ const [shadowRoot, setShadowRoot] = useState(
+ defaultTechDocsReaderPageValue.shadowRoot,
+ );
+
+ const value = {
+ metadata,
+ entityName,
+ entityMetadata,
+ shadowRoot,
+ setShadowRoot,
+ title,
+ setTitle,
+ subtitle,
+ setSubtitle,
+ };
+
+ return (
+
+ {children instanceof Function ? children(value) : children}
+
+ );
+ },
+ (prevProps, nextProps) => {
+ return areEntityNamesEqual(prevProps.entityName, nextProps.entityName);
+ },
+);
+
+/**
+ * Hook for use within TechDocs addons to retrieve Entity Metadata for the
+ * current TechDocs site.
+ * @public
+ */
+export const useEntityMetadata = () => {
+ const { entityMetadata } = useTechDocsReaderPage();
+ return entityMetadata;
+};
+
+/**
+ * Hook for use within TechDocs addons to retrieve TechDocs Metadata for the
+ * current TechDocs site.
+ * @public
+ */
+export const useTechDocsMetadata = () => {
+ const { metadata } = useTechDocsReaderPage();
+ return metadata;
};
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx
index 0feeb65e12..4a823a9e7c 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useRef, useState, useEffect, useCallback } from 'react';
import { create } from 'jss';
import { makeStyles, Grid, Portal } from '@material-ui/core';
@@ -29,7 +29,9 @@ import { Content, Progress } from '@backstage/core-components';
import { TechDocsSearch } from '../../../search';
import { useTechDocsReaderPage } from '../TechDocsReaderPage';
import { TechDocsStateIndicator } from '../TechDocsStateIndicator';
-import { useTechDocsReaderDom, withTechDocsReaderProvider } from './context';
+
+import { useTechDocsReaderDom } from './dom';
+import { withTechDocsReaderProvider } from './context';
const useStyles = makeStyles({
search: {
@@ -49,10 +51,10 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider(
({ withSearch = true }: TechDocsReaderPageContentProps) => {
const classes = useStyles();
const addons = useTechDocsAddons();
- const page = useTechDocsReaderPage();
- const dom = useTechDocsReaderDom(page.entityName);
+ const { entityName, setShadowRoot } = useTechDocsReaderPage();
+ const dom = useTechDocsReaderDom(entityName);
- const ref = useRef(null);
+ const ref = useRef(null);
const [jss, setJss] = useState(
create({
...jssPreset(),
@@ -75,16 +77,16 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider(
shadowHost.shadowRoot ?? shadowHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '';
shadowRoot.appendChild(dom);
- page.setShadowRoot(shadowRoot);
- }, [dom, page]);
+ setShadowRoot(shadowRoot);
+ }, [dom, setShadowRoot]);
- const contentElement = ref.current?.shadowRoot?.querySelector(
+ const contentElement = ref.current?.querySelector(
'[data-md-component="container"]',
);
- const primarySidebarElement = ref.current?.shadowRoot?.querySelector(
+ const primarySidebarElement = ref.current?.querySelector(
'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]',
);
- const secondarySidebarElement = ref.current?.shadowRoot?.querySelector(
+ const secondarySidebarElement = ref.current?.querySelector(
'div[data-md-component="sidebar"][data-md-type="toc"], div[data-md-component="toc"]',
);
@@ -111,7 +113,7 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider(
{withSearch && (
-
+
)}
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx
index 7a4877d3cd..e1d6a0d55f 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/context.tsx
@@ -15,42 +15,14 @@
*/
import React, {
- FC,
ComponentType,
createContext,
useContext,
- useCallback,
- useEffect,
- useState,
+ ReactNode,
} from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
-import { useTheme, Theme } from '@material-ui/core';
-import { lighten, alpha } from '@material-ui/core/styles';
-
-import { BackstageTheme } from '@backstage/theme';
import { CompoundEntityRef } from '@backstage/catalog-model';
-import { useApi, configApiRef } from '@backstage/core-plugin-api';
-import { SidebarPinStateContext } from '@backstage/core-components';
-import { scmIntegrationsApiRef } from '@backstage/integration-react';
-
-import { techdocsStorageApiRef } from '../../../api';
-
-import {
- addBaseUrl,
- addGitFeedbackLink,
- addLinkClickListener,
- addSidebarToggle,
- injectCss,
- onCssReady,
- removeMkdocsHeader,
- rewriteDocLinks,
- sanitizeDOM,
- simplifyMkdocsFooter,
- scrollIntoAnchor,
- transform as transformer,
- copyToClipboard,
-} from '../../transformers';
import { useReaderState } from '../useReaderState';
import { useTechDocsReaderPage } from '../TechDocsReaderPage';
@@ -72,13 +44,37 @@ const TechDocsReaderContext = createContext(
{} as TechDocsReaderValue,
);
-export const TechDocsReaderProvider: FC = ({ children }) => {
- const { path, entityName } = useTechDocsReaderPage();
+/**
+ * Note: this hook is currently being exported so that we can rapidly
+ * iterate on alternative implementations that extend core
+ * functionality. There is no guarantee that this hook will continue to be
+ * exported by the package in the future!
+ *
+ * todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
+ * @internal
+ */
+
+export const useTechDocsReader = () => useContext(TechDocsReaderContext);
+
+type TechDocsReaderProviderRenderFunction = (
+ value: TechDocsReaderValue,
+) => JSX.Element;
+
+type TechDocsReaderProviderProps = {
+ children: TechDocsReaderProviderRenderFunction | ReactNode;
+};
+
+export const TechDocsReaderProvider = ({
+ children,
+}: TechDocsReaderProviderProps) => {
+ const { '*': path = '' } = useParams();
+ const { entityName } = useTechDocsReaderPage();
const { kind, namespace, name } = entityName;
const value = useReaderState(kind, namespace, name, path);
+
return (
- {children}
+ {children instanceof Function ? children(value) : children}
);
};
@@ -100,758 +96,3 @@ export const withTechDocsReaderProvider =
);
-
-/**
- * Note: this hook is currently being exported so that we can rapidly
- * iterate on alternative implementations that extend core
- * functionality. There is no guarantee that this hook will continue to be
- * exported by the package in the future!
- *
- * todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
- * @internal
- */
-export const useTechDocsReader = () => useContext(TechDocsReaderContext);
-
-type TypographyHeadings = Pick<
- Theme['typography'],
- 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
->;
-
-type TypographyHeadingsKeys = keyof TypographyHeadings;
-
-const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
-
-/**
- * Hook that encapsulates the behavior of getting raw HTML and applying
- * transforms to it in order to make it function at a basic level in the
- * Backstage UI.
- *
- * Note: this hook is currently being exported so that we can rapidly iterate
- * on alternative implementations that extend core functionality.
- * There is no guarantee that this hook will continue to be exported by the
- * package in the future!
- *
- * todo: Make public or stop exporting (see others: "altReaderExperiments")
- * @internal
- */
-export const useTechDocsReaderDom = (
- entityRef: CompoundEntityRef,
-): Element | null => {
- const navigate = useNavigate();
- const theme = useTheme();
- const techdocsStorageApi = useApi(techdocsStorageApiRef);
- const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
- const techdocsSanitizer = useApi(configApiRef);
- const { namespace = '', kind = '', name = '' } = entityRef;
- const { state, path, content: rawPage } = useTechDocsReader();
- const isDarkTheme = theme.palette.type === 'dark';
-
- const [sidebars, setSidebars] = useState();
- const [dom, setDom] = useState(null);
-
- // sidebar pinned status to be used in computing CSS style injections
- const { isPinned } = useContext(SidebarPinStateContext);
-
- const updateSidebarPosition = useCallback(() => {
- if (!dom || !sidebars) return;
- // set sidebar height so they don't initially render in wrong position
- const mdTabs = dom.querySelector('.md-container > .md-tabs');
- const sidebarsCollapsed = window.matchMedia(
- 'screen and (max-width: 76.1875em)',
- ).matches;
- const newTop = Math.max(dom.getBoundingClientRect().top, 0);
- sidebars.forEach(sidebar => {
- if (sidebarsCollapsed) {
- sidebar.style.top = '0px';
- } else if (mdTabs) {
- sidebar.style.top = `${
- newTop + mdTabs.getBoundingClientRect().height
- }px`;
- } else {
- sidebar.style.top = `${newTop}px`;
- }
- });
- }, [dom, sidebars]);
-
- useEffect(() => {
- updateSidebarPosition();
- window.addEventListener('scroll', updateSidebarPosition, true);
- window.addEventListener('resize', updateSidebarPosition);
- return () => {
- window.removeEventListener('scroll', updateSidebarPosition, true);
- window.removeEventListener('resize', updateSidebarPosition);
- };
- // an update to "state" might lead to an updated UI so we include it as a trigger
- }, [updateSidebarPosition, state]);
-
- // dynamically set width of footer to accommodate for pinning of the sidebar
- const updateFooterWidth = useCallback(() => {
- if (!dom) return;
- const footer = dom.querySelector('.md-footer') as HTMLElement;
- if (footer) {
- footer.style.width = `${dom.getBoundingClientRect().width}px`;
- }
- }, [dom]);
-
- useEffect(() => {
- updateFooterWidth();
- window.addEventListener('resize', updateFooterWidth);
- return () => {
- window.removeEventListener('resize', updateFooterWidth);
- };
- });
-
- // a function that performs transformations that are executed prior to adding it to the DOM
- const preRender = useCallback(
- (rawContent: string, contentPath: string) =>
- transformer(rawContent, [
- sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
- addBaseUrl({
- techdocsStorageApi,
- entityId: {
- kind,
- name,
- namespace,
- },
- path: contentPath,
- }),
- rewriteDocLinks(),
- addSidebarToggle(),
- removeMkdocsHeader(),
- simplifyMkdocsFooter(),
- addGitFeedbackLink(scmIntegrationsApi),
- injectCss({
- // Variables
- css: `
- /*
- As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host.
- As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them.
- */
- :host {
- /* FONT */
- --md-default-fg-color: ${theme.palette.text.primary};
- --md-default-fg-color--light: ${theme.palette.text.secondary};
- --md-default-fg-color--lighter: ${lighten(
- theme.palette.text.secondary,
- 0.7,
- )};
- --md-default-fg-color--lightest: ${lighten(
- theme.palette.text.secondary,
- 0.3,
- )};
-
- /* BACKGROUND */
- --md-default-bg-color:${theme.palette.background.default};
- --md-default-bg-color--light: ${theme.palette.background.paper};
- --md-default-bg-color--lighter: ${lighten(
- theme.palette.background.paper,
- 0.7,
- )};
- --md-default-bg-color--lightest: ${lighten(
- theme.palette.background.paper,
- 0.3,
- )};
-
- /* PRIMARY */
- --md-primary-fg-color: ${theme.palette.primary.main};
- --md-primary-fg-color--light: ${theme.palette.primary.light};
- --md-primary-fg-color--dark: ${theme.palette.primary.dark};
- --md-primary-bg-color: ${theme.palette.primary.contrastText};
- --md-primary-bg-color--light: ${lighten(
- theme.palette.primary.contrastText,
- 0.7,
- )};
-
- /* ACCENT */
- --md-accent-fg-color: var(--md-primary-fg-color);
-
- /* SHADOW */
- --md-shadow-z1: ${theme.shadows[1]};
- --md-shadow-z2: ${theme.shadows[2]};
- --md-shadow-z3: ${theme.shadows[3]};
-
- /* EXTENSIONS */
- --md-admonition-fg-color: var(--md-default-fg-color);
- --md-admonition-bg-color: var(--md-default-bg-color);
- /* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */
- --md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,');
- --md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,');
- --md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-details-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,');
- --md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,');
- --md-nav-icon--next: url('data:image/svg+xml;charset=utf-8,');
- --md-toc-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-clipboard-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-search-result-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-source-version-icon: url('data:image/svg+xml;charset=utf-8,');
- --md-version-icon: url('data:image/svg+xml;charset=utf-8,');
- }
-
- :host > * {
- /* CODE */
- --md-code-fg-color: ${theme.palette.text.primary};
- --md-code-bg-color: ${theme.palette.background.paper};
- --md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
- --md-code-hl-keyword-color: ${
- isDarkTheme
- ? theme.palette.primary.light
- : theme.palette.primary.dark
- };
- --md-code-hl-function-color: ${
- isDarkTheme
- ? theme.palette.secondary.light
- : theme.palette.secondary.dark
- };
- --md-code-hl-string-color: ${
- isDarkTheme
- ? theme.palette.success.light
- : theme.palette.success.dark
- };
- --md-code-hl-number-color: ${
- isDarkTheme
- ? theme.palette.error.light
- : theme.palette.error.dark
- };
- --md-code-hl-constant-color: var(--md-code-hl-function-color);
- --md-code-hl-special-color: var(--md-code-hl-function-color);
- --md-code-hl-name-color: var(--md-code-fg-color);
- --md-code-hl-comment-color: var(--md-default-fg-color--light);
- --md-code-hl-generic-color: var(--md-default-fg-color--light);
- --md-code-hl-variable-color: var(--md-default-fg-color--light);
- --md-code-hl-operator-color: var(--md-default-fg-color--light);
- --md-code-hl-punctuation-color: var(--md-default-fg-color--light);
-
- /* TYPESET */
- --md-typeset-font-size: 1rem;
- --md-typeset-color: var(--md-default-fg-color);
- --md-typeset-a-color: var(--md-accent-fg-color);
- --md-typeset-table-color: ${theme.palette.text.primary};
- --md-typeset-del-color: ${
- isDarkTheme
- ? alpha(theme.palette.error.dark, 0.5)
- : alpha(theme.palette.error.light, 0.5)
- };
- --md-typeset-ins-color: ${
- isDarkTheme
- ? alpha(theme.palette.success.dark, 0.5)
- : alpha(theme.palette.success.light, 0.5)
- };
- --md-typeset-mark-color: ${
- isDarkTheme
- ? alpha(theme.palette.warning.dark, 0.5)
- : alpha(theme.palette.warning.light, 0.5)
- };
- }
-
- @media screen and (max-width: 76.1875em) {
- :host > * {
- /* TYPESET */
- --md-typeset-font-size: .9rem;
- }
- }
-
- @media screen and (max-width: 600px) {
- :host > * {
- /* TYPESET */
- --md-typeset-font-size: .7rem;
- }
- }
- `,
- }),
- injectCss({
- // Reset
- css: `
- body {
- --md-text-color: var(--md-default-fg-color);
- --md-text-link-color: var(--md-accent-fg-color);
- --md-text-font-family: ${theme.typography.fontFamily};
- font-family: var(--md-text-font-family);
- background-color: unset;
- }
- `,
- }),
- injectCss({
- // Layout
- css: `
- .md-grid {
- max-width: 100%;
- margin: 0;
- }
-
- .md-nav {
- font-size: calc(var(--md-typeset-font-size) * 0.9);
- }
- .md-nav__link {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .md-nav__icon {
- height: 20px !important;
- width: 20px !important;
- margin-left:${theme.spacing(1)}px;
- }
- .md-nav__icon svg {
- margin: 0;
- width: 20px !important;
- height: 20px !important;
- }
- .md-nav__icon:after {
- width: 20px !important;
- height: 20px !important;
- }
-
- .md-main__inner {
- margin-top: 0;
- }
-
- .md-sidebar {
- bottom: 75px;
- position: fixed;
- width: 16rem;
- overflow-y: auto;
- overflow-x: hidden;
- scrollbar-color: rgb(193, 193, 193) #eee;
- scrollbar-width: thin;
- }
- .md-sidebar .md-sidebar__scrollwrap {
- width: calc(16rem - 10px);
- }
- .md-sidebar--secondary {
- right: ${theme.spacing(3)}px;
- }
- .md-sidebar::-webkit-scrollbar {
- width: 5px;
- }
- .md-sidebar::-webkit-scrollbar-button {
- width: 5px;
- height: 5px;
- }
- .md-sidebar::-webkit-scrollbar-track {
- background: #eee;
- border: 1 px solid rgb(250, 250, 250);
- box-shadow: 0px 0px 3px #dfdfdf inset;
- border-radius: 3px;
- }
- .md-sidebar::-webkit-scrollbar-thumb {
- width: 5px;
- background: rgb(193, 193, 193);
- border: transparent;
- border-radius: 3px;
- }
- .md-sidebar::-webkit-scrollbar-thumb:hover {
- background: rgb(125, 125, 125);
- }
-
- .md-content {
- max-width: calc(100% - 16rem * 2);
- margin-left: 16rem;
- margin-bottom: 50px;
- }
-
- .md-footer {
- position: fixed;
- bottom: 0px;
- }
- .md-footer__title {
- background-color: unset;
- }
- .md-footer-nav__link {
- width: 16rem;
- }
-
- .md-dialog {
- background-color: unset;
- }
-
- @media screen and (min-width: 76.25em) {
- .md-sidebar {
- height: auto;
- }
- }
-
- @media screen and (max-width: 76.1875em) {
- .md-nav {
- transition: none !important;
- background-color: var(--md-default-bg-color)
- }
- .md-nav--primary .md-nav__title {
- cursor: auto;
- color: var(--md-default-fg-color);
- font-weight: 700;
- white-space: normal;
- line-height: 1rem;
- height: auto;
- display: flex;
- flex-flow: column;
- row-gap: 1.6rem;
- padding: 1.2rem .8rem .8rem;
- background-color: var(--md-default-bg-color);
- }
- .md-nav--primary .md-nav__title~.md-nav__list {
- box-shadow: none;
- }
- .md-nav--primary .md-nav__title ~ .md-nav__list > :first-child {
- border-top: none;
- }
- .md-nav--primary .md-nav__title .md-nav__button {
- display: none;
- }
- .md-nav--primary .md-nav__title .md-nav__icon {
- color: var(--md-default-fg-color);
- position: static;
- height: auto;
- margin: 0 0 0 -0.2rem;
- }
- .md-nav--primary > .md-nav__title [for="none"] {
- padding-top: 0;
- }
- .md-nav--primary .md-nav__item {
- border-top: none;
- }
- .md-nav--primary :is(.md-nav__title,.md-nav__item) {
- font-size : var(--md-typeset-font-size);
- }
- .md-nav .md-source {
- display: none;
- }
-
- .md-sidebar {
- height: 100%;
- }
- .md-sidebar--primary {
- width: 12.1rem !important;
- z-index: 200;
- left: ${
- isPinned
- ? 'calc(-12.1rem + 242px)'
- : 'calc(-12.1rem + 72px)'
- } !important;
- }
- .md-sidebar--secondary:not([hidden]) {
- display: none;
- }
-
- .md-content {
- max-width: 100%;
- margin-left: 0;
- }
-
- .md-header__button {
- margin: 0.4rem 0;
- margin-left: 0.4rem;
- padding: 0;
- }
-
- .md-overlay {
- left: 0;
- }
-
- .md-footer {
- position: static;
- padding-left: 0;
- }
- .md-footer-nav__link {
- /* footer links begin to overlap at small sizes without setting width */
- width: 50%;
- }
- }
-
- @media screen and (max-width: 600px) {
- .md-sidebar--primary {
- left: -12.1rem !important;
- width: 12.1rem;
- }
- }
- `,
- }),
- injectCss({
- // Typeset
- css: `
- .md-typeset {
- font-size: var(--md-typeset-font-size);
- }
-
- ${headings.reduce((style, heading) => {
- const styles = theme.typography[heading];
- const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
- const calculate = (value: typeof fontSize) => {
- let factor: number | string = 1;
- if (typeof value === 'number') {
- // 60% of the size defined because it is too big
- factor = (value / 16) * 0.6;
- }
- if (typeof value === 'string') {
- factor = value.replace('rem', '');
- }
- return `calc(${factor} * var(--md-typeset-font-size))`;
- };
- return style.concat(`
- .md-typeset ${heading} {
- color: var(--md-default-fg-color);
- line-height: ${lineHeight};
- font-family: ${fontFamily};
- font-weight: ${fontWeight};
- font-size: ${calculate(fontSize)};
- }
- `);
- }, '')}
-
- .md-typeset .md-content__button {
- color: var(--md-default-fg-color);
- }
-
- .md-typeset hr {
- border-bottom: 0.05rem dotted ${theme.palette.divider};
- }
-
- .md-typeset details {
- font-size: var(--md-typeset-font-size) !important;
- }
- .md-typeset details summary {
- padding-left: 2.5rem !important;
- }
- .md-typeset details summary:before,
- .md-typeset details summary:after {
- top: 50% !important;
- width: 20px !important;
- height: 20px !important;
- transform: rotate(0deg) translateY(-50%) !important;
- }
- .md-typeset details[open] > summary:after {
- transform: rotate(90deg) translateX(-50%) !important;
- }
-
- .md-typeset blockquote {
- color: var(--md-default-fg-color--light);
- border-left: 0.2rem solid var(--md-default-fg-color--light);
- }
-
- .md-typeset table:not([class]) {
- font-size: var(--md-typeset-font-size);
- border: 1px solid var(--md-default-fg-color);
- border-bottom: none;
- border-collapse: collapse;
- }
- .md-typeset table:not([class]) th {
- font-weight: bold;
- }
- .md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
- border-bottom: 1px solid var(--md-default-fg-color);
- }
-
- .md-typeset pre > code::-webkit-scrollbar-thumb {
- background-color: hsla(0, 0%, 0%, 0.32);
- }
- .md-typeset pre > code::-webkit-scrollbar-thumb:hover {
- background-color: hsla(0, 0%, 0%, 0.87);
- }
- `,
- }),
- injectCss({
- // Animations
- css: `
- /*
- Disable CSS animations on link colors as they lead to issues in dark mode.
- The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages.
- */
- .md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
- transition: none;
- }
- `,
- }),
- injectCss({
- // Extensions
- css: `
- /* HIGHLIGHT */
- .highlight .md-clipboard:after {
- content: unset;
- }
-
- .highlight .nx {
- color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
- }
-
- /* CODE HILITE */
- .codehilite .gd {
- background-color: ${
- isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
- };
- }
-
- .codehilite .gi {
- background-color: ${
- isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
- };
- }
-
- /* TABBED */
- .tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1),
- .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),
- .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),
- .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),
- .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),
- .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),
- .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),
- .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),
- .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),
- .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),
- .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),
- .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),
- .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),
- .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),
- .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),
- .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),
- .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),
- .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),
- .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),
- .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) {
- color: var(--md-accent-fg-color);
- border-color: var(--md-accent-fg-color);
- }
-
- /* TASK-LIST */
- .task-list-control .task-list-indicator::before {
- background-color: ${theme.palette.action.disabledBackground};
- }
- .task-list-control [type="checkbox"]:checked + .task-list-indicator:before {
- background-color: ${theme.palette.success.main};
- }
-
- /* ADMONITION */
- .admonition {
- font-size: var(--md-typeset-font-size) !important;
- }
- .admonition .admonition-title {
- padding-left: 2.5rem !important;
- }
-
- .admonition .admonition-title:before {
- top: 50% !important;
- width: 20px !important;
- height: 20px !important;
- transform: translateY(-50%) !important;
- }
- `,
- }),
- ]),
- [
- kind,
- name,
- namespace,
- scmIntegrationsApi,
- techdocsSanitizer,
- techdocsStorageApi,
- theme,
- isDarkTheme,
- isPinned,
- ],
- );
-
- // a function that performs transformations that are executed after adding it to the DOM
- const postRender = useCallback(
- async (transformedElement: Element) =>
- transformer(transformedElement, [
- scrollIntoAnchor(),
- copyToClipboard(theme),
- addLinkClickListener({
- baseUrl: window.location.origin,
- onClick: (event: MouseEvent, url: string) => {
- // detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
- const modifierActive = event.ctrlKey || event.metaKey;
- const parsedUrl = new URL(url);
-
- // hash exists when anchor is clicked on secondary sidebar
- if (parsedUrl.hash) {
- if (modifierActive) {
- window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
- } else {
- navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
- // Scroll to hash if it's on the current page
- transformedElement
- ?.querySelector(`#${parsedUrl.hash.slice(1)}`)
- ?.scrollIntoView();
- }
- } else {
- if (modifierActive) {
- window.open(parsedUrl.pathname, '_blank');
- } else {
- navigate(parsedUrl.pathname);
- // Scroll to top of reader if primary sidebar link is clicked
- transformedElement
- ?.querySelector('.md-content__inner')
- ?.scrollIntoView();
- }
- }
- },
- }),
- onCssReady({
- docStorageUrl: await techdocsStorageApi.getApiOrigin(),
- onLoading: (renderedElement: Element) => {
- (renderedElement as HTMLElement).style.setProperty('opacity', '0');
- },
- onLoaded: (renderedElement: Element) => {
- (renderedElement as HTMLElement).style.removeProperty('opacity');
- // disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
- renderedElement
- .querySelector('.md-nav__title')
- ?.removeAttribute('for');
- setSidebars(
- Array.from(renderedElement.querySelectorAll('.md-sidebar')),
- );
- },
- }),
- ]),
- [theme, navigate, techdocsStorageApi],
- );
-
- useEffect(() => {
- if (!rawPage) return () => {};
-
- // if false, there is already a newer execution of this effect
- let shouldReplaceContent = true;
-
- // Pre-render
- preRender(rawPage, path).then(async preTransformedDomElement => {
- if (!preTransformedDomElement?.innerHTML) {
- return; // An unexpected error occurred
- }
-
- // don't manipulate the shadow dom if this isn't the latest effect execution
- if (!shouldReplaceContent) {
- return;
- }
-
- // Scroll to top after render
- window.scroll({ top: 0 });
-
- // Post-render
- const postTransformedDomElement = await postRender(
- preTransformedDomElement,
- );
- setDom(postTransformedDomElement as HTMLElement);
- });
-
- // cancel this execution
- return () => {
- shouldReplaceContent = false;
- };
- }, [rawPage, path, preRender, postRender]);
-
- return dom;
-};
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx
new file mode 100644
index 0000000000..a6cf79f1db
--- /dev/null
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx
@@ -0,0 +1,790 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useContext, useCallback, useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { useTheme, Theme } from '@material-ui/core';
+import { lighten, alpha } from '@material-ui/core/styles';
+
+import { BackstageTheme } from '@backstage/theme';
+import { CompoundEntityRef } from '@backstage/catalog-model';
+import { useApi, configApiRef } from '@backstage/core-plugin-api';
+import { SidebarPinStateContext } from '@backstage/core-components';
+import { scmIntegrationsApiRef } from '@backstage/integration-react';
+
+import { techdocsStorageApiRef } from '../../../api';
+
+import { useTechDocsReader } from './context';
+
+import {
+ addBaseUrl,
+ addGitFeedbackLink,
+ addLinkClickListener,
+ addSidebarToggle,
+ injectCss,
+ onCssReady,
+ removeMkdocsHeader,
+ rewriteDocLinks,
+ sanitizeDOM,
+ simplifyMkdocsFooter,
+ scrollIntoAnchor,
+ transform as transformer,
+ copyToClipboard,
+} from '../../transformers';
+
+type TypographyHeadings = Pick<
+ Theme['typography'],
+ 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
+>;
+type TypographyHeadingsKeys = keyof TypographyHeadings;
+
+const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+
+/**
+ * Hook that encapsulates the behavior of getting raw HTML and applying
+ * transforms to it in order to make it function at a basic level in the
+ * Backstage UI.
+ *
+ * Note: this hook is currently being exported so that we can rapidly iterate
+ * on alternative implementations that extend core functionality.
+ * There is no guarantee that this hook will continue to be exported by the
+ * package in the future!
+ *
+ * todo: Make public or stop exporting (see others: "altReaderExperiments")
+ * @internal
+ */
+export const useTechDocsReaderDom = (
+ entityRef: CompoundEntityRef,
+): Element | null => {
+ const navigate = useNavigate();
+ const theme = useTheme();
+ const techdocsStorageApi = useApi(techdocsStorageApiRef);
+ const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
+ const techdocsSanitizer = useApi(configApiRef);
+ const { namespace = '', kind = '', name = '' } = entityRef;
+ const { state, path, content: rawPage } = useTechDocsReader();
+ const isDarkTheme = theme.palette.type === 'dark';
+
+ const [sidebars, setSidebars] = useState();
+ const [dom, setDom] = useState(null);
+
+ // sidebar pinned status to be used in computing CSS style injections
+ const { isPinned } = useContext(SidebarPinStateContext);
+
+ const updateSidebarPosition = useCallback(() => {
+ if (!dom || !sidebars) return;
+ // set sidebar height so they don't initially render in wrong position
+ const mdTabs = dom.querySelector('.md-container > .md-tabs');
+ const sidebarsCollapsed = window.matchMedia(
+ 'screen and (max-width: 76.1875em)',
+ ).matches;
+ const newTop = Math.max(dom.getBoundingClientRect().top, 0);
+ sidebars.forEach(sidebar => {
+ if (sidebarsCollapsed) {
+ sidebar.style.top = '0px';
+ } else if (mdTabs) {
+ sidebar.style.top = `${
+ newTop + mdTabs.getBoundingClientRect().height
+ }px`;
+ } else {
+ sidebar.style.top = `${newTop}px`;
+ }
+ });
+ }, [dom, sidebars]);
+
+ useEffect(() => {
+ updateSidebarPosition();
+ window.addEventListener('scroll', updateSidebarPosition, true);
+ window.addEventListener('resize', updateSidebarPosition);
+ return () => {
+ window.removeEventListener('scroll', updateSidebarPosition, true);
+ window.removeEventListener('resize', updateSidebarPosition);
+ };
+ // an update to "state" might lead to an updated UI so we include it as a trigger
+ }, [updateSidebarPosition, state]);
+
+ // dynamically set width of footer to accommodate for pinning of the sidebar
+ const updateFooterWidth = useCallback(() => {
+ if (!dom) return;
+ const footer = dom.querySelector('.md-footer') as HTMLElement;
+ if (footer) {
+ footer.style.width = `${dom.getBoundingClientRect().width}px`;
+ }
+ }, [dom]);
+
+ useEffect(() => {
+ updateFooterWidth();
+ window.addEventListener('resize', updateFooterWidth);
+ return () => {
+ window.removeEventListener('resize', updateFooterWidth);
+ };
+ });
+
+ // a function that performs transformations that are executed prior to adding it to the DOM
+ const preRender = useCallback(
+ (rawContent: string, contentPath: string) =>
+ transformer(rawContent, [
+ sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
+ addBaseUrl({
+ techdocsStorageApi,
+ entityId: {
+ kind,
+ name,
+ namespace,
+ },
+ path: contentPath,
+ }),
+ rewriteDocLinks(),
+ addSidebarToggle(),
+ removeMkdocsHeader(),
+ simplifyMkdocsFooter(),
+ addGitFeedbackLink(scmIntegrationsApi),
+ injectCss({
+ // Variables
+ css: `
+ /*
+ As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host.
+ As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them.
+ */
+ :host {
+ /* FONT */
+ --md-default-fg-color: ${theme.palette.text.primary};
+ --md-default-fg-color--light: ${theme.palette.text.secondary};
+ --md-default-fg-color--lighter: ${lighten(
+ theme.palette.text.secondary,
+ 0.7,
+ )};
+ --md-default-fg-color--lightest: ${lighten(
+ theme.palette.text.secondary,
+ 0.3,
+ )};
+
+ /* BACKGROUND */
+ --md-default-bg-color:${theme.palette.background.default};
+ --md-default-bg-color--light: ${theme.palette.background.paper};
+ --md-default-bg-color--lighter: ${lighten(
+ theme.palette.background.paper,
+ 0.7,
+ )};
+ --md-default-bg-color--lightest: ${lighten(
+ theme.palette.background.paper,
+ 0.3,
+ )};
+
+ /* PRIMARY */
+ --md-primary-fg-color: ${theme.palette.primary.main};
+ --md-primary-fg-color--light: ${theme.palette.primary.light};
+ --md-primary-fg-color--dark: ${theme.palette.primary.dark};
+ --md-primary-bg-color: ${theme.palette.primary.contrastText};
+ --md-primary-bg-color--light: ${lighten(
+ theme.palette.primary.contrastText,
+ 0.7,
+ )};
+
+ /* ACCENT */
+ --md-accent-fg-color: var(--md-primary-fg-color);
+
+ /* SHADOW */
+ --md-shadow-z1: ${theme.shadows[1]};
+ --md-shadow-z2: ${theme.shadows[2]};
+ --md-shadow-z3: ${theme.shadows[3]};
+
+ /* EXTENSIONS */
+ --md-admonition-fg-color: var(--md-default-fg-color);
+ --md-admonition-bg-color: var(--md-default-bg-color);
+ /* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */
+ --md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,');
+ --md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,');
+ --md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-details-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,');
+ --md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,');
+ --md-nav-icon--next: url('data:image/svg+xml;charset=utf-8,');
+ --md-toc-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-clipboard-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-search-result-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-source-version-icon: url('data:image/svg+xml;charset=utf-8,');
+ --md-version-icon: url('data:image/svg+xml;charset=utf-8,');
+ }
+
+ :host > * {
+ /* CODE */
+ --md-code-fg-color: ${theme.palette.text.primary};
+ --md-code-bg-color: ${theme.palette.background.paper};
+ --md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
+ --md-code-hl-keyword-color: ${
+ isDarkTheme
+ ? theme.palette.primary.light
+ : theme.palette.primary.dark
+ };
+ --md-code-hl-function-color: ${
+ isDarkTheme
+ ? theme.palette.secondary.light
+ : theme.palette.secondary.dark
+ };
+ --md-code-hl-string-color: ${
+ isDarkTheme
+ ? theme.palette.success.light
+ : theme.palette.success.dark
+ };
+ --md-code-hl-number-color: ${
+ isDarkTheme
+ ? theme.palette.error.light
+ : theme.palette.error.dark
+ };
+ --md-code-hl-constant-color: var(--md-code-hl-function-color);
+ --md-code-hl-special-color: var(--md-code-hl-function-color);
+ --md-code-hl-name-color: var(--md-code-fg-color);
+ --md-code-hl-comment-color: var(--md-default-fg-color--light);
+ --md-code-hl-generic-color: var(--md-default-fg-color--light);
+ --md-code-hl-variable-color: var(--md-default-fg-color--light);
+ --md-code-hl-operator-color: var(--md-default-fg-color--light);
+ --md-code-hl-punctuation-color: var(--md-default-fg-color--light);
+
+ /* TYPESET */
+ --md-typeset-font-size: 1rem;
+ --md-typeset-color: var(--md-default-fg-color);
+ --md-typeset-a-color: var(--md-accent-fg-color);
+ --md-typeset-table-color: ${theme.palette.text.primary};
+ --md-typeset-del-color: ${
+ isDarkTheme
+ ? alpha(theme.palette.error.dark, 0.5)
+ : alpha(theme.palette.error.light, 0.5)
+ };
+ --md-typeset-ins-color: ${
+ isDarkTheme
+ ? alpha(theme.palette.success.dark, 0.5)
+ : alpha(theme.palette.success.light, 0.5)
+ };
+ --md-typeset-mark-color: ${
+ isDarkTheme
+ ? alpha(theme.palette.warning.dark, 0.5)
+ : alpha(theme.palette.warning.light, 0.5)
+ };
+ }
+
+ @media screen and (max-width: 76.1875em) {
+ :host > * {
+ /* TYPESET */
+ --md-typeset-font-size: .9rem;
+ }
+ }
+
+ @media screen and (max-width: 600px) {
+ :host > * {
+ /* TYPESET */
+ --md-typeset-font-size: .7rem;
+ }
+ }
+ `,
+ }),
+ injectCss({
+ // Reset
+ css: `
+ body {
+ --md-text-color: var(--md-default-fg-color);
+ --md-text-link-color: var(--md-accent-fg-color);
+ --md-text-font-family: ${theme.typography.fontFamily};
+ font-family: var(--md-text-font-family);
+ background-color: unset;
+ }
+ `,
+ }),
+ injectCss({
+ // Layout
+ css: `
+ .md-grid {
+ max-width: 100%;
+ margin: 0;
+ }
+
+ .md-nav {
+ font-size: calc(var(--md-typeset-font-size) * 0.9);
+ }
+ .md-nav__link {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .md-nav__icon {
+ height: 20px !important;
+ width: 20px !important;
+ margin-left:${theme.spacing(1)}px;
+ }
+ .md-nav__icon svg {
+ margin: 0;
+ width: 20px !important;
+ height: 20px !important;
+ }
+ .md-nav__icon:after {
+ width: 20px !important;
+ height: 20px !important;
+ }
+
+ .md-main__inner {
+ margin-top: 0;
+ }
+
+ .md-sidebar {
+ bottom: 75px;
+ position: fixed;
+ width: 16rem;
+ overflow-y: auto;
+ overflow-x: hidden;
+ scrollbar-color: rgb(193, 193, 193) #eee;
+ scrollbar-width: thin;
+ }
+ .md-sidebar .md-sidebar__scrollwrap {
+ width: calc(16rem - 10px);
+ }
+ .md-sidebar--secondary {
+ right: ${theme.spacing(3)}px;
+ }
+ .md-sidebar::-webkit-scrollbar {
+ width: 5px;
+ }
+ .md-sidebar::-webkit-scrollbar-button {
+ width: 5px;
+ height: 5px;
+ }
+ .md-sidebar::-webkit-scrollbar-track {
+ background: #eee;
+ border: 1 px solid rgb(250, 250, 250);
+ box-shadow: 0px 0px 3px #dfdfdf inset;
+ border-radius: 3px;
+ }
+ .md-sidebar::-webkit-scrollbar-thumb {
+ width: 5px;
+ background: rgb(193, 193, 193);
+ border: transparent;
+ border-radius: 3px;
+ }
+ .md-sidebar::-webkit-scrollbar-thumb:hover {
+ background: rgb(125, 125, 125);
+ }
+
+ .md-content {
+ max-width: calc(100% - 16rem * 2);
+ margin-left: 16rem;
+ margin-bottom: 50px;
+ }
+
+ .md-footer {
+ position: fixed;
+ bottom: 0px;
+ }
+ .md-footer__title {
+ background-color: unset;
+ }
+ .md-footer-nav__link {
+ width: 16rem;
+ }
+
+ .md-dialog {
+ background-color: unset;
+ }
+
+ @media screen and (min-width: 76.25em) {
+ .md-sidebar {
+ height: auto;
+ }
+ }
+
+ @media screen and (max-width: 76.1875em) {
+ .md-nav {
+ transition: none !important;
+ background-color: var(--md-default-bg-color)
+ }
+ .md-nav--primary .md-nav__title {
+ cursor: auto;
+ color: var(--md-default-fg-color);
+ font-weight: 700;
+ white-space: normal;
+ line-height: 1rem;
+ height: auto;
+ display: flex;
+ flex-flow: column;
+ row-gap: 1.6rem;
+ padding: 1.2rem .8rem .8rem;
+ background-color: var(--md-default-bg-color);
+ }
+ .md-nav--primary .md-nav__title~.md-nav__list {
+ box-shadow: none;
+ }
+ .md-nav--primary .md-nav__title ~ .md-nav__list > :first-child {
+ border-top: none;
+ }
+ .md-nav--primary .md-nav__title .md-nav__button {
+ display: none;
+ }
+ .md-nav--primary .md-nav__title .md-nav__icon {
+ color: var(--md-default-fg-color);
+ position: static;
+ height: auto;
+ margin: 0 0 0 -0.2rem;
+ }
+ .md-nav--primary > .md-nav__title [for="none"] {
+ padding-top: 0;
+ }
+ .md-nav--primary .md-nav__item {
+ border-top: none;
+ }
+ .md-nav--primary :is(.md-nav__title,.md-nav__item) {
+ font-size : var(--md-typeset-font-size);
+ }
+ .md-nav .md-source {
+ display: none;
+ }
+
+ .md-sidebar {
+ height: 100%;
+ }
+ .md-sidebar--primary {
+ width: 12.1rem !important;
+ z-index: 200;
+ left: ${
+ isPinned
+ ? 'calc(-12.1rem + 242px)'
+ : 'calc(-12.1rem + 72px)'
+ } !important;
+ }
+ .md-sidebar--secondary:not([hidden]) {
+ display: none;
+ }
+
+ .md-content {
+ max-width: 100%;
+ margin-left: 0;
+ }
+
+ .md-header__button {
+ margin: 0.4rem 0;
+ margin-left: 0.4rem;
+ padding: 0;
+ }
+
+ .md-overlay {
+ left: 0;
+ }
+
+ .md-footer {
+ position: static;
+ padding-left: 0;
+ }
+ .md-footer-nav__link {
+ /* footer links begin to overlap at small sizes without setting width */
+ width: 50%;
+ }
+ }
+
+ @media screen and (max-width: 600px) {
+ .md-sidebar--primary {
+ left: -12.1rem !important;
+ width: 12.1rem;
+ }
+ }
+ `,
+ }),
+ injectCss({
+ // Typeset
+ css: `
+ .md-typeset {
+ font-size: var(--md-typeset-font-size);
+ }
+
+ ${headings.reduce((style, heading) => {
+ const styles = theme.typography[heading];
+ const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
+ const calculate = (value: typeof fontSize) => {
+ let factor: number | string = 1;
+ if (typeof value === 'number') {
+ // 60% of the size defined because it is too big
+ factor = (value / 16) * 0.6;
+ }
+ if (typeof value === 'string') {
+ factor = value.replace('rem', '');
+ }
+ return `calc(${factor} * var(--md-typeset-font-size))`;
+ };
+ return style.concat(`
+ .md-typeset ${heading} {
+ color: var(--md-default-fg-color);
+ line-height: ${lineHeight};
+ font-family: ${fontFamily};
+ font-weight: ${fontWeight};
+ font-size: ${calculate(fontSize)};
+ }
+ `);
+ }, '')}
+
+ .md-typeset .md-content__button {
+ color: var(--md-default-fg-color);
+ }
+
+ .md-typeset hr {
+ border-bottom: 0.05rem dotted ${theme.palette.divider};
+ }
+
+ .md-typeset details {
+ font-size: var(--md-typeset-font-size) !important;
+ }
+ .md-typeset details summary {
+ padding-left: 2.5rem !important;
+ }
+ .md-typeset details summary:before,
+ .md-typeset details summary:after {
+ top: 50% !important;
+ width: 20px !important;
+ height: 20px !important;
+ transform: rotate(0deg) translateY(-50%) !important;
+ }
+ .md-typeset details[open] > summary:after {
+ transform: rotate(90deg) translateX(-50%) !important;
+ }
+
+ .md-typeset blockquote {
+ color: var(--md-default-fg-color--light);
+ border-left: 0.2rem solid var(--md-default-fg-color--light);
+ }
+
+ .md-typeset table:not([class]) {
+ font-size: var(--md-typeset-font-size);
+ border: 1px solid var(--md-default-fg-color);
+ border-bottom: none;
+ border-collapse: collapse;
+ }
+ .md-typeset table:not([class]) th {
+ font-weight: bold;
+ }
+ .md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
+ border-bottom: 1px solid var(--md-default-fg-color);
+ }
+
+ .md-typeset pre > code::-webkit-scrollbar-thumb {
+ background-color: hsla(0, 0%, 0%, 0.32);
+ }
+ .md-typeset pre > code::-webkit-scrollbar-thumb:hover {
+ background-color: hsla(0, 0%, 0%, 0.87);
+ }
+ `,
+ }),
+ injectCss({
+ // Animations
+ css: `
+ /*
+ Disable CSS animations on link colors as they lead to issues in dark mode.
+ The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages.
+ */
+ .md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
+ transition: none;
+ }
+ `,
+ }),
+ injectCss({
+ // Extensions
+ css: `
+ /* HIGHLIGHT */
+ .highlight .md-clipboard:after {
+ content: unset;
+ }
+
+ .highlight .nx {
+ color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
+ }
+
+ /* CODE HILITE */
+ .codehilite .gd {
+ background-color: ${
+ isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
+ };
+ }
+
+ .codehilite .gi {
+ background-color: ${
+ isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
+ };
+ }
+
+ /* TABBED */
+ .tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1),
+ .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),
+ .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),
+ .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),
+ .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),
+ .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),
+ .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),
+ .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),
+ .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),
+ .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),
+ .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),
+ .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),
+ .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),
+ .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),
+ .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),
+ .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),
+ .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),
+ .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),
+ .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),
+ .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) {
+ color: var(--md-accent-fg-color);
+ border-color: var(--md-accent-fg-color);
+ }
+
+ /* TASK-LIST */
+ .task-list-control .task-list-indicator::before {
+ background-color: ${theme.palette.action.disabledBackground};
+ }
+ .task-list-control [type="checkbox"]:checked + .task-list-indicator:before {
+ background-color: ${theme.palette.success.main};
+ }
+
+ /* ADMONITION */
+ .admonition {
+ font-size: var(--md-typeset-font-size) !important;
+ }
+ .admonition .admonition-title {
+ padding-left: 2.5rem !important;
+ }
+
+ .admonition .admonition-title:before {
+ top: 50% !important;
+ width: 20px !important;
+ height: 20px !important;
+ transform: translateY(-50%) !important;
+ }
+ `,
+ }),
+ ]),
+ [
+ kind,
+ name,
+ namespace,
+ scmIntegrationsApi,
+ techdocsSanitizer,
+ techdocsStorageApi,
+ theme,
+ isDarkTheme,
+ isPinned,
+ ],
+ );
+
+ // a function that performs transformations that are executed after adding it to the DOM
+ const postRender = useCallback(
+ async (transformedElement: Element) =>
+ transformer(transformedElement, [
+ scrollIntoAnchor(),
+ copyToClipboard(theme),
+ addLinkClickListener({
+ baseUrl: window.location.origin,
+ onClick: (event: MouseEvent, url: string) => {
+ // detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
+ const modifierActive = event.ctrlKey || event.metaKey;
+ const parsedUrl = new URL(url);
+
+ // hash exists when anchor is clicked on secondary sidebar
+ if (parsedUrl.hash) {
+ if (modifierActive) {
+ window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
+ } else {
+ navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
+ // Scroll to hash if it's on the current page
+ transformedElement
+ ?.querySelector(`#${parsedUrl.hash.slice(1)}`)
+ ?.scrollIntoView();
+ }
+ } else {
+ if (modifierActive) {
+ window.open(parsedUrl.pathname, '_blank');
+ } else {
+ navigate(parsedUrl.pathname);
+ // Scroll to top of reader if primary sidebar link is clicked
+ transformedElement
+ ?.querySelector('.md-content__inner')
+ ?.scrollIntoView();
+ }
+ }
+ },
+ }),
+ onCssReady({
+ docStorageUrl: await techdocsStorageApi.getApiOrigin(),
+ onLoading: (renderedElement: Element) => {
+ (renderedElement as HTMLElement).style.setProperty('opacity', '0');
+ },
+ onLoaded: (renderedElement: Element) => {
+ (renderedElement as HTMLElement).style.removeProperty('opacity');
+ // disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
+ renderedElement
+ .querySelector('.md-nav__title')
+ ?.removeAttribute('for');
+ setSidebars(
+ Array.from(renderedElement.querySelectorAll('.md-sidebar')),
+ );
+ },
+ }),
+ ]),
+ [theme, navigate, techdocsStorageApi],
+ );
+
+ useEffect(() => {
+ if (!rawPage) return () => {};
+
+ // if false, there is already a newer execution of this effect
+ let shouldReplaceContent = true;
+
+ // Pre-render
+ preRender(rawPage, path).then(async preTransformedDomElement => {
+ if (!preTransformedDomElement?.innerHTML) {
+ return; // An unexpected error occurred
+ }
+
+ // don't manipulate the shadow dom if this isn't the latest effect execution
+ if (!shouldReplaceContent) {
+ return;
+ }
+
+ // Scroll to top after render
+ window.scroll({ top: 0 });
+
+ // Post-render
+ const postTransformedDomElement = await postRender(
+ preTransformedDomElement,
+ );
+ setDom(postTransformedDomElement as HTMLElement);
+ });
+
+ // cancel this execution
+ return () => {
+ shouldReplaceContent = false;
+ };
+ }, [rawPage, path, preRender, postRender]);
+
+ return dom;
+};
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/index.ts b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/index.ts
index d3da3d6aa4..288969e73b 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/index.ts
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/index.ts
@@ -16,3 +16,4 @@
export { TechDocsReaderPageContent } from './TechDocsReaderPageContent';
export * from './context';
+export * from './dom';
diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx
index c417274403..3f9a5b9e1f 100644
--- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx
@@ -33,11 +33,7 @@ import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { Header, HeaderLabel } from '@backstage/core-components';
import { useRouteRef, configApiRef, useApi } from '@backstage/core-plugin-api';
-import {
- useTechDocsReaderPage,
- useTechDocsMetadata,
- useEntityMetadata,
-} from '../TechDocsReaderPage';
+import { useTechDocsReaderPage } from '../TechDocsReaderPage';
import { rootRouteRef } from '../../../routes';
@@ -47,31 +43,30 @@ export const TechDocsReaderPageHeader: FC = ({ children }) => {
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
- const { value: entityMetadata } = useEntityMetadata();
- const { value: techDocsMetadata } = useTechDocsMetadata();
-
const {
title,
setTitle,
subtitle,
setSubtitle,
- entityName: entityRef,
+ entityName,
+ metadata: { value: metadata },
+ entityMetadata: { value: entityMetadata },
} = useTechDocsReaderPage();
useEffect(() => {
- if (!techDocsMetadata) return;
+ if (!metadata) return;
setTitle(prevTitle => {
- const { site_name } = techDocsMetadata;
+ const { site_name } = metadata;
return prevTitle || site_name;
});
setSubtitle(prevSubtitle => {
- let { site_description } = techDocsMetadata;
+ let { site_description } = metadata;
if (site_description === 'None') {
site_description = 'Home';
}
return prevSubtitle || site_description;
});
- }, [techDocsMetadata, setTitle, setSubtitle]);
+ }, [metadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const tabTitle = [subtitle, title, appTitle].filter(Boolean).join(' | ');
@@ -92,7 +87,7 @@ export const TechDocsReaderPageHeader: FC = ({ children }) => {
value={
}