Merge pull request #1468 from spotify/mob/fetching-mock-docs
feat(mkdocs-reader): initial navigation inside docs
This commit is contained in:
@@ -29,6 +29,8 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "^6.0.0-alpha.5",
|
||||
"react-router-dom": "^6.0.0-alpha.5",
|
||||
"react-use": "^14.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export const docStorageURL =
|
||||
'https://techdocs-mock-sites.storage.googleapis.com';
|
||||
@@ -33,7 +33,7 @@ import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { Reader } from './reader/components/Reader';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/docs',
|
||||
path: '/docs/:componentId/*',
|
||||
title: 'Docs',
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
import React from 'react';
|
||||
import { useShadowDom } from '..';
|
||||
import { useAsync } from 'react-use';
|
||||
import transformer, { addBaseUrl, rewriteDocLinks } from '../transformers';
|
||||
import { docStorageURL } from '../../config';
|
||||
import { Link } from '@backstage/core';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import URLParser from '../urlParser';
|
||||
|
||||
const useFetch = (url: string) => {
|
||||
const state = useAsync(async () => {
|
||||
@@ -28,54 +33,59 @@ const useFetch = (url: string) => {
|
||||
return state;
|
||||
};
|
||||
|
||||
const addBaseUrl = (htmlString: string, baseUrl: string): string => {
|
||||
const domParser = new DOMParser().parseFromString(htmlString, 'text/html');
|
||||
const normalizeUrl = (path: string) => {
|
||||
return path.replace(/\/\/index.html$/, '/index.html');
|
||||
};
|
||||
|
||||
const updateDom = <T extends Element>(
|
||||
list: Array<T>,
|
||||
attributeName: string,
|
||||
): void => {
|
||||
Array.from(list).forEach((elem: T) => {
|
||||
const newUrl = new URL(
|
||||
elem.getAttribute(attributeName)!,
|
||||
baseUrl,
|
||||
).toString();
|
||||
elem.setAttribute(attributeName, newUrl);
|
||||
});
|
||||
};
|
||||
const useEnforcedTrailingSlash = (): void => {
|
||||
React.useEffect(() => {
|
||||
const actualUrl = window.location.href;
|
||||
const expectedUrl = new URLParser(window.location.href, '.').parse();
|
||||
|
||||
updateDom<HTMLImageElement>(Array.from(domParser.images), 'src');
|
||||
updateDom<HTMLAnchorElement | HTMLAreaElement>(
|
||||
Array.from(domParser.links),
|
||||
'href',
|
||||
);
|
||||
updateDom<HTMLLinkElement>(
|
||||
Array.from(domParser.querySelectorAll('link')),
|
||||
'href',
|
||||
);
|
||||
|
||||
return domParser.body.parentElement?.outerHTML || htmlString;
|
||||
if (actualUrl !== expectedUrl) {
|
||||
window.history.replaceState({}, document.title, expectedUrl);
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const Reader = () => {
|
||||
const location = useLocation();
|
||||
const { componentId, '*': path } = useParams();
|
||||
const shadowDomRef = useShadowDom();
|
||||
const state = useFetch(
|
||||
'https://techdocs-mock-sites.storage.googleapis.com/mkdocs/index.html',
|
||||
normalizeUrl(
|
||||
`${docStorageURL}${location.pathname.replace('/docs', '')}/index.html`,
|
||||
),
|
||||
);
|
||||
|
||||
useEnforcedTrailingSlash();
|
||||
|
||||
React.useEffect(() => {
|
||||
const divElement = shadowDomRef.current;
|
||||
if (divElement?.shadowRoot && state.value) {
|
||||
divElement.shadowRoot.innerHTML = addBaseUrl(
|
||||
state.value,
|
||||
'https://techdocs-mock-sites.storage.googleapis.com/mkdocs/',
|
||||
);
|
||||
const transformedElement = transformer(state.value, [
|
||||
addBaseUrl({
|
||||
docStorageURL,
|
||||
componentId,
|
||||
path,
|
||||
}),
|
||||
rewriteDocLinks({
|
||||
componentId,
|
||||
}),
|
||||
]);
|
||||
|
||||
divElement.shadowRoot.innerHTML = '';
|
||||
if (transformedElement)
|
||||
divElement.shadowRoot.appendChild(transformedElement);
|
||||
}
|
||||
}, [shadowDomRef, state]);
|
||||
}, [shadowDomRef, state, componentId, path]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Shadow DOM should be underneath</h3>
|
||||
<nav>
|
||||
<Link to="/docs/mkdocs/">mkdocs</Link>
|
||||
<Link to="/docs/backstage-microsite/">Backstage docs</Link>
|
||||
</nav>
|
||||
<div ref={shadowDomRef} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 URLParser from '../urlParser';
|
||||
|
||||
type AddBaseUrlOptions = {
|
||||
docStorageURL: string;
|
||||
componentId: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const addBaseUrl = ({
|
||||
docStorageURL,
|
||||
componentId,
|
||||
path,
|
||||
}: AddBaseUrlOptions) => {
|
||||
return (dom: Document): Document => {
|
||||
const updateDom = <T extends Element>(
|
||||
list: Array<T>,
|
||||
attributeName: string,
|
||||
): void => {
|
||||
Array.from(list)
|
||||
.filter(elem => !!elem.getAttribute(attributeName))
|
||||
.forEach((elem: T) => {
|
||||
const newUrl = new URLParser(
|
||||
`${docStorageURL}/${componentId}/${path}`,
|
||||
elem.getAttribute(attributeName)!,
|
||||
).parse();
|
||||
elem.setAttribute(attributeName, newUrl);
|
||||
});
|
||||
};
|
||||
|
||||
updateDom<HTMLImageElement>(Array.from(dom.images), 'src');
|
||||
updateDom<HTMLScriptElement>(Array.from(dom.scripts), 'src');
|
||||
updateDom<HTMLLinkElement>(
|
||||
Array.from(dom.querySelectorAll('link')),
|
||||
'href',
|
||||
);
|
||||
|
||||
return dom;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './addBaseUrl';
|
||||
export * from './rewriteDocLinks';
|
||||
|
||||
type Transformer = (dom: Document) => Document;
|
||||
|
||||
export default (
|
||||
html: string,
|
||||
transformers: Transformer[],
|
||||
): HTMLElement | undefined => {
|
||||
const dom = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
transformers.forEach(transformer => transformer(dom));
|
||||
|
||||
return dom.body.parentElement ?? undefined;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 URLParser from '../urlParser';
|
||||
|
||||
type AddBaseUrlOptions = {};
|
||||
|
||||
export const rewriteDocLinks = ({}: AddBaseUrlOptions) => {
|
||||
return (dom: Document): Document => {
|
||||
const updateDom = <T extends Element>(
|
||||
list: Array<T>,
|
||||
attributeName: string,
|
||||
): void => {
|
||||
Array.from(list)
|
||||
.filter(elem => elem.hasAttribute(attributeName))
|
||||
.forEach((elem: T) => {
|
||||
elem.setAttribute(
|
||||
attributeName,
|
||||
new URLParser(
|
||||
window.location.href,
|
||||
elem.getAttribute(attributeName)!,
|
||||
).parse(),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
updateDom(Array.from(dom.getElementsByTagName('a')), 'href');
|
||||
|
||||
return dom;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 URLParser from './urlParser';
|
||||
|
||||
describe('URLParser', () => {
|
||||
it('should not change an absolute url', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.google.com/',
|
||||
'https://www.mkdocs.org/',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual('https://www.mkdocs.org/');
|
||||
});
|
||||
|
||||
it('should convert a relative url to an absolute url', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
'../../support/installing/',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual(
|
||||
'https://www.mkdocs.org/support/installing/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a trailing slash', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.mkdocs.org/user-guide/getting-started',
|
||||
'.',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
const urlParser = new URLParser(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
'.',
|
||||
);
|
||||
|
||||
expect(urlParser.parse()).toEqual(
|
||||
'https://www.mkdocs.org/user-guide/getting-started/',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const normalizeBaseURL = (baseURL: string): string => {
|
||||
const url = new URL(baseURL);
|
||||
url.pathname = url.pathname.replace(/([^/])$/, '$1/');
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export default class URLParser {
|
||||
constructor(public baseURL: string, public pathname: string) {
|
||||
this.baseURL = normalizeBaseURL(baseURL);
|
||||
}
|
||||
|
||||
parse(): string {
|
||||
return new URL(this.pathname, this.baseURL).toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user