diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 9ef290fa40..ca8e957a46 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -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": { diff --git a/plugins/techdocs/src/config.js b/plugins/techdocs/src/config.js new file mode 100644 index 0000000000..296b9fc14c --- /dev/null +++ b/plugins/techdocs/src/config.js @@ -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'; diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 23bfa3b714..fec2b1cf74 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -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', }); diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 5517c0c715..d4e84aa8d5 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -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 = ( - list: Array, - 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(Array.from(domParser.images), 'src'); - updateDom( - Array.from(domParser.links), - 'href', - ); - updateDom( - 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 ( <> -

Shadow DOM should be underneath

+
); diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts new file mode 100644 index 0000000000..e07a41fc03 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -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 = ( + list: Array, + 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(Array.from(dom.images), 'src'); + updateDom(Array.from(dom.scripts), 'src'); + updateDom( + Array.from(dom.querySelectorAll('link')), + 'href', + ); + + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts new file mode 100644 index 0000000000..008c86e261 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -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; +}; diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts new file mode 100644 index 0000000000..373db51abd --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -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 = ( + list: Array, + 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; + }; +}; diff --git a/plugins/techdocs/src/reader/urlParser.test.ts b/plugins/techdocs/src/reader/urlParser.test.ts new file mode 100644 index 0000000000..4322fc42e1 --- /dev/null +++ b/plugins/techdocs/src/reader/urlParser.test.ts @@ -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/', + ); + }); +}); diff --git a/plugins/techdocs/src/reader/urlParser.ts b/plugins/techdocs/src/reader/urlParser.ts new file mode 100644 index 0000000000..b1429b7635 --- /dev/null +++ b/plugins/techdocs/src/reader/urlParser.ts @@ -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(); + } +}