From 71aa8d9d18da4b54a0f5f0e0f4d3a3c4ad8d8642 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 14:00:28 +0200 Subject: [PATCH 1/6] Scroll to anchor link --- plugins/techdocs/src/reader/components/Reader.tsx | 13 ++++++++++--- ...istener.test.ts => addLinkClickListener.test.ts} | 6 +++--- ...{addEventListener.ts => addLinkClickListener.ts} | 10 ++++------ .../src/reader/transformers/rewriteDocLinks.test.ts | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) rename plugins/techdocs/src/reader/transformers/{addEventListener.test.ts => addLinkClickListener.test.ts} (89%) rename plugins/techdocs/src/reader/transformers/{addEventListener.ts => addLinkClickListener.ts} (81%) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 12cec30c80..935def0dd4 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -25,7 +25,7 @@ import { Header, Content, ItemCard } from '@backstage/core'; import transformer, { addBaseUrl, rewriteDocLinks, - addEventListener, + addLinkClickListener, removeMkdocsHeader, modifyCss, } from '../transformers'; @@ -92,8 +92,15 @@ export const Reader = () => { if (transformedElement) { divElement.shadowRoot.appendChild(transformedElement); transformer(divElement.shadowRoot.children[0], [ - addEventListener({ - onClick: navigate, + addLinkClickListener({ + onClick: url => { + const parsedUrl = new URL(url); + navigate(parsedUrl.pathname); + + divElement.shadowRoot + ?.querySelector(parsedUrl.hash) + ?.scrollIntoView(); + }, }), ]); } diff --git a/plugins/techdocs/src/reader/transformers/addEventListener.test.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts similarity index 89% rename from plugins/techdocs/src/reader/transformers/addEventListener.test.ts rename to plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts index e87ab81ab5..0554ff85e9 100644 --- a/plugins/techdocs/src/reader/transformers/addEventListener.test.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts @@ -15,14 +15,14 @@ */ import { createTestShadowDom, FIXTURES } from '../../test-utils'; -import { addEventListener } from '../transformers'; +import { addLinkClickListener } from '.'; -describe('addEventListener', () => { +describe('addLinkClickListener', () => { it('calls onClick when a link has been clicked', () => { const fn = jest.fn(); const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { transformers: [ - addEventListener({ + addLinkClickListener({ onClick: fn, }), ], diff --git a/plugins/techdocs/src/reader/transformers/addEventListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts similarity index 81% rename from plugins/techdocs/src/reader/transformers/addEventListener.ts rename to plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index 32b9d6b0b9..fe0a11d9ae 100644 --- a/plugins/techdocs/src/reader/transformers/addEventListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -16,22 +16,20 @@ import type { Transformer } from './index'; -type AddEventListenerOptions = { +type addLinkClickListenerOptions = { onClick: (newUrl: string) => void; }; -export const addEventListener = ({ +export const addLinkClickListener = ({ onClick, -}: AddEventListenerOptions): Transformer => { +}: addLinkClickListenerOptions): Transformer => { return dom => { Array.from(dom.getElementsByTagName('a')).forEach(elem => { elem.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); const target = e.target as HTMLAnchorElement; if (target?.getAttribute('href')) { - onClick( - target.getAttribute('href')!.replace(window.location.origin, ''), - ); + onClick(target.getAttribute('href')!); } }); }); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 83ca846142..a064511fd0 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -34,7 +34,7 @@ describe('rewriteDocLinks', () => { ]); }); - it('should transform a href with licalhost as baseUrl', () => { + it('should transform a href with localhost as baseUrl', () => { const shadowDom = createTestShadowDom( ` Test From ef1c5813bf902c8df067af61581d900540b4c201 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 14:04:33 +0200 Subject: [PATCH 2/6] Change type name --- .../techdocs/src/reader/transformers/addLinkClickListener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index fe0a11d9ae..83fb79a0b4 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -16,13 +16,13 @@ import type { Transformer } from './index'; -type addLinkClickListenerOptions = { +type AddLinkClickListenerOptions = { onClick: (newUrl: string) => void; }; export const addLinkClickListener = ({ onClick, -}: addLinkClickListenerOptions): Transformer => { +}: AddLinkClickListenerOptions): Transformer => { return dom => { Array.from(dom.getElementsByTagName('a')).forEach(elem => { elem.addEventListener('click', (e: MouseEvent) => { From 6f9270e7656f1dc638df70307b7a95f96ad2c2c2 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 14:11:57 +0200 Subject: [PATCH 3/6] Updated export from --- plugins/techdocs/src/reader/transformers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index f49f496277..f35d6aa478 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -16,7 +16,7 @@ export * from './addBaseUrl'; export * from './rewriteDocLinks'; -export * from './addEventListener'; +export * from './addLinkClickListener'; export * from './removeMkdocsHeader'; export * from './modifyCss'; From 45a981ca0442dabb1c43081f05c0e778dd293906 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 15:39:35 +0200 Subject: [PATCH 4/6] Rewrite urlParser to urlFormater --- .../techdocs/src/reader/components/Reader.tsx | 23 +++-- .../src/reader/transformers/addBaseUrl.ts | 12 +-- .../transformers/addLinkClickListener.ts | 4 +- .../reader/transformers/rewriteDocLinks.ts | 8 +- .../techdocs/src/reader/urlFormatter.test.ts | 84 +++++++++++++++++++ .../reader/{urlParser.ts => urlFormatter.ts} | 28 ++++--- plugins/techdocs/src/reader/urlParser.test.ts | 61 -------------- 7 files changed, 130 insertions(+), 90 deletions(-) create mode 100644 plugins/techdocs/src/reader/urlFormatter.test.ts rename plugins/techdocs/src/reader/{urlParser.ts => urlFormatter.ts} (50%) delete mode 100644 plugins/techdocs/src/reader/urlParser.test.ts diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 935def0dd4..6398d86515 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -30,7 +30,7 @@ import transformer, { modifyCss, } from '../transformers'; import { docStorageURL } from '../../config'; -import URLParser from '../urlParser'; +import URLFormatter from '../urlFormatter'; const useFetch = (url: string) => { const state = useAsync(async () => { @@ -45,7 +45,7 @@ const useFetch = (url: string) => { const useEnforcedTrailingSlash = (): void => { React.useEffect(() => { const actualUrl = window.location.href; - const expectedUrl = new URLParser(window.location.href, '.').parse(); + const expectedUrl = new URLFormatter(window.location.href).formatBaseURL(); if (actualUrl !== expectedUrl) { window.history.replaceState({}, document.title, expectedUrl); @@ -58,10 +58,9 @@ export const Reader = () => { const { componentId, '*': path } = useParams(); const shadowDomRef = useShadowDom(); const navigate = useNavigate(); - const normalizedUrl = new URLParser( + const normalizedUrl = new URLFormatter( `${docStorageURL}${location.pathname.replace('/docs', '')}`, - '.', - ).parse(); + ).formatBaseURL(); const state = useFetch(`${normalizedUrl}index.html`); useEnforcedTrailingSlash(); @@ -89,13 +88,23 @@ export const Reader = () => { ]); divElement.shadowRoot.innerHTML = ''; + if (transformedElement) { divElement.shadowRoot.appendChild(transformedElement); transformer(divElement.shadowRoot.children[0], [ + dom => { + setTimeout(() => { + if (window.location.hash) { + const hash = window.location.hash.slice(1); + divElement.shadowRoot?.getElementById(hash)?.scrollIntoView(); + } + }, 200); + return dom; + }, addLinkClickListener({ - onClick: url => { + onClick: (_: MouseEvent, url: string) => { const parsedUrl = new URL(url); - navigate(parsedUrl.pathname); + navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); divElement.shadowRoot ?.querySelector(parsedUrl.hash) diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index 653f0b2f71..dc041e9a71 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import URLParser from '../urlParser'; +import URLFormatter from '../urlFormatter'; import type { Transformer } from './index'; type AddBaseUrlOptions = { @@ -36,11 +36,13 @@ export const addBaseUrl = ({ Array.from(list) .filter(elem => !!elem.getAttribute(attributeName)) .forEach((elem: T) => { - const newUrl = new URLParser( + const urlFormatter = new URLFormatter( `${docStorageURL}/${componentId}/${path}`, - elem.getAttribute(attributeName)!, - ).parse(); - elem.setAttribute(attributeName, newUrl); + ); + elem.setAttribute( + attributeName, + urlFormatter.formatURL(elem.getAttribute(attributeName)!), + ); }); }; diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index 83fb79a0b4..93e4b67a1b 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -17,7 +17,7 @@ import type { Transformer } from './index'; type AddLinkClickListenerOptions = { - onClick: (newUrl: string) => void; + onClick: (e: MouseEvent, newUrl: string) => void; }; export const addLinkClickListener = ({ @@ -29,7 +29,7 @@ export const addLinkClickListener = ({ e.preventDefault(); const target = e.target as HTMLAnchorElement; if (target?.getAttribute('href')) { - onClick(target.getAttribute('href')!); + onClick(e, target.getAttribute('href')!); } }); }); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index 7b8495b6b7..4bf4ab3898 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import URLParser from '../urlParser'; +import URLFormatter from '../urlFormatter'; import type { Transformer } from './index'; export const rewriteDocLinks = (): Transformer => { @@ -26,12 +26,10 @@ export const rewriteDocLinks = (): Transformer => { Array.from(list) .filter(elem => elem.hasAttribute(attributeName)) .forEach((elem: T) => { + const urlFormatter = new URLFormatter(window.location.href); elem.setAttribute( attributeName, - new URLParser( - window.location.href, - elem.getAttribute(attributeName)!, - ).parse(), + urlFormatter.formatURL(elem.getAttribute(attributeName)!), ); }); }; diff --git a/plugins/techdocs/src/reader/urlFormatter.test.ts b/plugins/techdocs/src/reader/urlFormatter.test.ts new file mode 100644 index 0000000000..2659f9f4dc --- /dev/null +++ b/plugins/techdocs/src/reader/urlFormatter.test.ts @@ -0,0 +1,84 @@ +/* + * 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 URLFormatter from './urlFormatter'; + +describe('URLFormatter', () => { + describe('formatURL', () => { + it('should not change an absolute url', () => { + const formatter = new URLFormatter('https://www.google.com/'); + expect(formatter.formatURL('https://www.mkdocs.org/')).toEqual( + 'https://www.mkdocs.org/', + ); + }); + + it('should convert a relative url to an absolute url', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + expect(formatter.formatURL('../../support/installing/')).toEqual( + 'https://www.mkdocs.org/support/installing/', + ); + }); + + it('should add a trailing slash', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started', + ); + expect(formatter.formatURL('./getting-started')).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + }); + + it('should not add a trailing slash', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + expect(formatter.formatURL('.')).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + }); + + it('should not add multiple hashes', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/#hash1', + ); + expect(formatter.formatURL('./#hash2')).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/#hash2', + ); + }); + }); + + describe('formatBaseURL', () => { + it('should keep query params in URL', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world', + ); + expect(formatter.formatBaseURL()).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world', + ); + }); + + it('should keep hash in URL', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/#hash', + ); + expect(formatter.formatBaseURL()).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/#hash', + ); + }); + }); +}); diff --git a/plugins/techdocs/src/reader/urlParser.ts b/plugins/techdocs/src/reader/urlFormatter.ts similarity index 50% rename from plugins/techdocs/src/reader/urlParser.ts rename to plugins/techdocs/src/reader/urlFormatter.ts index b1429b7635..166f0d9963 100644 --- a/plugins/techdocs/src/reader/urlParser.ts +++ b/plugins/techdocs/src/reader/urlFormatter.ts @@ -14,18 +14,26 @@ * 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 URLFormatter { + constructor(public baseURL: string) {} -export default class URLParser { - constructor(public baseURL: string, public pathname: string) { - this.baseURL = normalizeBaseURL(baseURL); + formatBaseURL(): string { + return this.normalizeURL(this.baseURL); } - parse(): string { - return new URL(this.pathname, this.baseURL).toString(); + formatURL(pathname: string): string { + return this.normalizeURL(new URL(pathname, this.baseURL).toString()); + } + + private normalizeURL(urlString: string): string { + const url = new URL(urlString); + const filename: string = url.pathname.split('/').pop() ?? url.pathname; + const isDir: boolean = filename.includes('.') === false; + + if (isDir) { + url.pathname = url.pathname.replace(/([^/])$/, '$1/'); + } + + return url.toString(); } } diff --git a/plugins/techdocs/src/reader/urlParser.test.ts b/plugins/techdocs/src/reader/urlParser.test.ts deleted file mode 100644 index 4322fc42e1..0000000000 --- a/plugins/techdocs/src/reader/urlParser.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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/', - ); - }); -}); From b2ff69e2d30788122ffc77218787491f8b3a2e7b Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 1 Jul 2020 10:01:57 +0200 Subject: [PATCH 5/6] fix(transformer): add trailing slash to test output --- .../src/reader/transformers/rewriteDocLinks.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index a064511fd0..b4ceab0b38 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -49,9 +49,9 @@ describe('rewriteDocLinks', () => { expect(getSample(shadowDom, 'a', 'href', 6)).toEqual([ 'http://example.org/', - 'http://localhost/example', - 'http://localhost/example-docs', - 'http://localhost/example-docs/example-page', + 'http://localhost/example/', + 'http://localhost/example-docs/', + 'http://localhost/example-docs/example-page/', ]); }); }); From 5e7eeda1d0e1ef788b959e82892346a12743a17c Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 1 Jul 2020 11:53:56 +0200 Subject: [PATCH 6/6] fix(transformers): add slash at end of path if missing --- .../reader/transformers/addBaseUrl.test.ts | 27 ++++++++++++++++++- .../src/reader/transformers/addBaseUrl.ts | 5 +++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index b9cf9745ee..4b1477e9c0 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -62,7 +62,7 @@ describe('addBaseUrl', () => { ]); }); - it('includes path option', () => { + it('includes path option without slash', () => { const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { transformers: [ addBaseUrl({ @@ -86,4 +86,29 @@ describe('addBaseUrl', () => { 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js', ]); }); + + it('includes path option with slash', () => { + const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + transformers: [ + addBaseUrl({ + docStorageURL: DOC_STORAGE_URL, + componentId: 'example-docs', + path: 'examplepath/', + }), + ], + }); + + expect(getSample(shadowDom, 'img', 'src')).toEqual([ + 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/win-py-install.png', + 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/initial-layout.png', + ]); + expect(getSample(shadowDom, 'link', 'href')).toEqual([ + 'https://www.mkdocs.org/', + 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/images/favicon.png', + ]); + expect(getSample(shadowDom, 'script', 'src')).toEqual([ + 'https://www.google-analytics.com/analytics.js', + 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js', + ]); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index dc041e9a71..91986db729 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -37,8 +37,11 @@ export const addBaseUrl = ({ .filter(elem => !!elem.getAttribute(attributeName)) .forEach((elem: T) => { const urlFormatter = new URLFormatter( - `${docStorageURL}/${componentId}/${path}`, + path.length < 1 || path.endsWith('/') + ? `${docStorageURL}/${componentId}/${path}` + : `${docStorageURL}/${componentId}/${path}/`, ); + elem.setAttribute( attributeName, urlFormatter.formatURL(elem.getAttribute(attributeName)!),