diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 6b879a2d78..65ea979607 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -31,7 +31,8 @@ "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", - "react-use": "^14.2.0" + "react-use": "^14.2.0", + "sanitize-html": "^1.27.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 1975d350e1..c42a3657cf 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -28,6 +28,7 @@ import transformer, { removeMkdocsHeader, modifyCss, onCssReady, + sanitizeDOM, } from '../transformers'; import { docStorageURL } from '../../config'; import URLFormatter from '../urlFormatter'; @@ -88,6 +89,7 @@ export const Reader = () => { // Pre-render const transformedElement = transformer(state.value as string, [ + sanitizeDOM(), addBaseUrl({ docStorageURL, componentId, diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 8629dce0d3..ceb2313b03 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -39,13 +39,14 @@ describe('addBaseUrl', () => { it('contains transformed absolute paths', () => { const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [ + preTransformers: [ addBaseUrl({ docStorageURL: DOC_STORAGE_URL, componentId: 'example-docs', path: '', }), ], + postTransformers: [], }); expect(getSample(shadowDom, 'img', 'src')).toEqual([ @@ -73,13 +74,14 @@ describe('addBaseUrl', () => { `, { - transformers: [ + preTransformers: [ addBaseUrl({ docStorageURL: DOC_STORAGE_URL, componentId: 'example-docs', path: 'examplepath', }), ], + postTransformers: [], }, ); @@ -108,13 +110,14 @@ describe('addBaseUrl', () => { `, { - transformers: [ + preTransformers: [ addBaseUrl({ docStorageURL: DOC_STORAGE_URL, componentId: 'example-docs', path: 'examplepath/', }), ], + postTransformers: [], }, ); diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts index 0554ff85e9..8e942b8be1 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts @@ -21,7 +21,8 @@ describe('addLinkClickListener', () => { it('calls onClick when a link has been clicked', () => { const fn = jest.fn(); const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [ + preTransformers: [], + postTransformers: [ addLinkClickListener({ onClick: fn, }), diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index 4e12dcae48..c8568282cf 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -20,6 +20,7 @@ export * from './addLinkClickListener'; export * from './removeMkdocsHeader'; export * from './modifyCss'; export * from './onCssReady'; +export * from './sanitizeDOM'; export type Transformer = (dom: Element) => Element; @@ -37,7 +38,9 @@ function transform( throw new Error('dom is not a recognized type'); } - transformers.forEach(transformer => transformer(dom)); + transformers.forEach(transformer => { + dom = transformer(dom); + }); return dom; } diff --git a/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx b/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx index fa808103dc..fff3869c96 100644 --- a/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx +++ b/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx @@ -22,7 +22,8 @@ describe('modifyCss', () => { const shadowDom = createTestShadowDom( `
`, { - transformers: [], + preTransformers: [], + postTransformers: [], }, ); @@ -37,13 +38,14 @@ describe('modifyCss', () => { const shadowDom = createTestShadowDom( ``, { - transformers: [ + preTransformers: [ modifyCss({ cssTransforms: { '.md-typeset': [{ 'font-size': '1em' }], }, }), ], + postTransformers: [], }, ); diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts index 10edbbae5e..234100fc4b 100644 --- a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts +++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts @@ -42,7 +42,8 @@ describe('onCssReady', () => { const onLoaded = jest.fn(); createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [ + preTransformers: [], + postTransformers: [ onCssReady({ docStorageURL, onLoading, @@ -61,7 +62,8 @@ describe('onCssReady', () => { const onLoaded = jest.fn(); createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [ + preTransformers: [], + postTransformers: [ addBaseUrl({ docStorageURL, componentId: 'mkdocs', diff --git a/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts b/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts index 7e155ea317..d7d0332be7 100644 --- a/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts +++ b/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.test.ts @@ -20,7 +20,8 @@ import { removeMkdocsHeader } from '../transformers'; describe('removeMkdocsHeader', () => { it('does not remove mkdocs header', () => { const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [], + preTransformers: [], + postTransformers: [], }); expect(shadowDom.querySelector('.md-header')).toBeTruthy(); @@ -28,7 +29,8 @@ describe('removeMkdocsHeader', () => { it('does remove mkdocs header', () => { const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [removeMkdocsHeader()], + preTransformers: [removeMkdocsHeader()], + postTransformers: [], }); expect(shadowDom.querySelector('.md-header')).toBeFalsy(); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index b4ceab0b38..20ec3ccb64 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -43,7 +43,8 @@ describe('rewriteDocLinks', () => { Test Sub Page `, { - transformers: [rewriteDocLinks()], + preTransformers: [rewriteDocLinks()], + postTransformers: [], }, ); diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM/attributes.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM/attributes.ts new file mode 100644 index 0000000000..e82e6929c3 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/sanitizeDOM/attributes.ts @@ -0,0 +1,386 @@ +/* + * 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. + */ + +/** + * This is the source of truth of what attributes we support in TechDocs. + */ + +export const html = [ + 'accept', + 'action', + 'align', + 'alt', + 'autocapitalize', + 'autocomplete', + 'autopictureinpicture', + 'autoplay', + 'background', + 'bgcolor', + 'border', + 'capture', + 'cellpadding', + 'cellspacing', + 'checked', + 'cite', + 'class', + 'clear', + 'color', + 'cols', + 'colspan', + 'controls', + 'controlslist', + 'coords', + 'crossorigin', + 'datetime', + 'decoding', + 'default', + 'dir', + 'disabled', + 'disablepictureinpicture', + 'disableremoteplayback', + 'download', + 'draggable', + 'enctype', + 'enterkeyhint', + 'face', + 'for', + 'headers', + 'height', + 'hidden', + 'high', + 'href', + 'hreflang', + 'id', + 'inputmode', + 'integrity', + 'ismap', + 'kind', + 'label', + 'lang', + 'list', + 'loading', + 'loop', + 'low', + 'max', + 'maxlength', + 'media', + 'method', + 'min', + 'minlength', + 'multiple', + 'muted', + 'name', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'optimum', + 'pattern', + 'placeholder', + 'playsinline', + 'poster', + 'preload', + 'pubdate', + 'radiogroup', + 'readonly', + 'rel', + 'required', + 'rev', + 'reversed', + 'role', + 'rows', + 'rowspan', + 'spellcheck', + 'scope', + 'selected', + 'shape', + 'size', + 'sizes', + 'span', + 'srclang', + 'start', + 'src', + 'srcset', + 'step', + 'style', + 'summary', + 'tabindex', + 'title', + 'translate', + 'type', + 'usemap', + 'valign', + 'value', + 'width', + 'xmlns', +]; + +export const svg = [ + 'accent-height', + 'accumulate', + 'additive', + 'alignment-baseline', + 'ascent', + 'attributename', + 'attributetype', + 'azimuth', + 'basefrequency', + 'baseline-shift', + 'begin', + 'bias', + 'by', + 'class', + 'clip', + 'clip-path', + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'cx', + 'cy', + 'd', + 'dx', + 'dy', + 'data', + 'diffuseconstant', + 'direction', + 'display', + 'divisor', + 'dur', + 'edgemode', + 'elevation', + 'end', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'filterunits', + 'flood-color', + 'flood-opacity', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'fx', + 'fy', + 'g1', + 'g2', + 'glyph-name', + 'glyphref', + 'gradientunits', + 'gradienttransform', + 'height', + 'href', + 'id', + 'image-rendering', + 'in', + 'in2', + 'k', + 'k1', + 'k2', + 'k3', + 'k4', + 'kerning', + 'keypoints', + 'keysplines', + 'keytimes', + 'lang', + 'lengthadjust', + 'letter-spacing', + 'kernelmatrix', + 'kernelunitlength', + 'lighting-color', + 'local', + 'marker-end', + 'marker-mid', + 'marker-start', + 'markerheight', + 'markerunits', + 'markerwidth', + 'maskcontentunits', + 'maskunits', + 'max', + 'mask', + 'media', + 'method', + 'mode', + 'min', + 'name', + 'numoctaves', + 'offset', + 'operator', + 'opacity', + 'order', + 'orient', + 'orientation', + 'origin', + 'overflow', + 'paint-order', + 'path', + 'pathlength', + 'patterncontentunits', + 'patterntransform', + 'patternunits', + 'points', + 'preservealpha', + 'preserveaspectratio', + 'primitiveunits', + 'r', + 'rx', + 'ry', + 'radius', + 'refx', + 'refy', + 'repeatcount', + 'repeatdur', + 'restart', + 'result', + 'rotate', + 'scale', + 'seed', + 'shape-rendering', + 'specularconstant', + 'specularexponent', + 'spreadmethod', + 'startoffset', + 'stddeviation', + 'stitchtiles', + 'stop-color', + 'stop-opacity', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke', + 'stroke-width', + 'style', + 'surfacescale', + 'tabindex', + 'targetx', + 'targety', + 'transform', + 'text-anchor', + 'text-decoration', + 'text-rendering', + 'textlength', + 'type', + 'u1', + 'u2', + 'unicode', + 'values', + 'viewbox', + 'visibility', + 'version', + 'vert-adv-y', + 'vert-origin-x', + 'vert-origin-y', + 'width', + 'word-spacing', + 'wrap', + 'writing-mode', + 'xchannelselector', + 'ychannelselector', + 'x', + 'x1', + 'x2', + 'xmlns', + 'y', + 'y1', + 'y2', + 'z', + 'zoomandpan', +]; + +export const mathMl = [ + 'accent', + 'accentunder', + 'align', + 'bevelled', + 'close', + 'columnsalign', + 'columnlines', + 'columnspan', + 'denomalign', + 'depth', + 'dir', + 'display', + 'displaystyle', + 'encoding', + 'fence', + 'frame', + 'height', + 'href', + 'id', + 'largeop', + 'length', + 'linethickness', + 'lspace', + 'lquote', + 'mathbackground', + 'mathcolor', + 'mathsize', + 'mathvariant', + 'maxsize', + 'minsize', + 'movablelimits', + 'notation', + 'numalign', + 'open', + 'rowalign', + 'rowlines', + 'rowspacing', + 'rowspan', + 'rspace', + 'rquote', + 'scriptlevel', + 'scriptminsize', + 'scriptsizemultiplier', + 'selection', + 'separator', + 'separators', + 'stretchy', + 'subscriptshift', + 'supscriptshift', + 'symmetric', + 'voffset', + 'width', + 'xmlns', +]; + +export const xml = [ + 'xlink:href', + 'xml:id', + 'xlink:title', + 'xml:space', + 'xmlns:xlink', +]; + +/** + * Anything in here will be supported as HTML attributes from TechDocs. + * + * @note Be conscious about what you add here. It can affect the TechDocs experience. For + * some review before making a PR, reach out in the #docs-like-code channel in Discord first. + */ +export const TECHDOCS_ALLOWED_ATTRIBUTES = { + '*': [...html, ...svg, ...mathMl, ...xml, 'data-*'], +}; diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts new file mode 100644 index 0000000000..9754437c1a --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { createTestShadowDom, FIXTURES } from '../../../test-utils'; +import { Transformer, sanitizeDOM } from '..'; + +const injectMaliciousLink = (): Transformer => dom => { + const link = document.createElement('a'); + link.setAttribute('id', 'test-malicious-link'); + link.setAttribute('onclick', 'alert("Hello world");'); + dom.querySelector('body')?.appendChild(link); + return dom; +}; + +describe('sanitizeDOM', () => { + it('contains a script tag', () => { + const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE); + + expect(shadowDom.querySelectorAll('script').length).toBeGreaterThan(0); + }); + + it('does not contain a script tag', () => { + const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + preTransformers: [sanitizeDOM()], + postTransformers: [], + }); + + expect(shadowDom.querySelectorAll('script').length).toBe(0); + }); + + it('contains link with a onClick attribute', () => { + const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + preTransformers: [injectMaliciousLink()], + postTransformers: [], + }); + + expect( + shadowDom.querySelector('#test-malicious-link')?.hasAttribute('onclick'), + ).toBeTruthy(); + }); + + it('does not contain link with a onClick attribute', () => { + const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + preTransformers: [sanitizeDOM()], + postTransformers: [], + }); + + expect( + shadowDom.querySelector('#test-malicious-link')?.hasAttribute('onclick'), + ).toBeFalsy(); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.ts new file mode 100644 index 0000000000..153e474e87 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/sanitizeDOM/index.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +// @ts-ignore +import sanitizeHtml from 'sanitize-html'; +import type { Transformer } from '..'; +import { TECHDOCS_ALLOWED_TAGS } from './tags'; +import { TECHDOCS_ALLOWED_ATTRIBUTES } from './attributes'; + +export const sanitizeDOM = (): Transformer => { + return dom => { + const sanitizedHtml = sanitizeHtml(dom.innerHTML, { + allowedTags: TECHDOCS_ALLOWED_TAGS, + allowedAttributes: TECHDOCS_ALLOWED_ATTRIBUTES, + allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'data', 'blob'], + }); + + return new DOMParser().parseFromString(sanitizedHtml, 'text/html') + .documentElement; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/sanitizeDOM/tags.ts b/plugins/techdocs/src/reader/transformers/sanitizeDOM/tags.ts new file mode 100644 index 0000000000..85823cbe8a --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/sanitizeDOM/tags.ts @@ -0,0 +1,251 @@ +/* + * 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. + */ + +/** + * This is the source of truth for what HTML tags we support in TechDocs. + */ + +// prettier-ignore +export const html = [ + 'a', 'abbr', 'acronym', + 'address', 'area', 'article', + 'aside', 'audio', 'b', 'bdi', + 'bdo', 'big', + 'blink', 'blockquote', + 'body', + 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', + 'col', 'colgroup', + 'content', + 'data', + 'datalist', + 'dd', + 'decorator', + 'del', + 'details', + 'dfn', + 'dir', + 'div', + 'dl', + 'dt', + 'element', + 'em', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'link', + 'main', + 'map', + 'mark', + 'marquee', + 'menu', + 'menuitem', + 'meter', + 'nav', + 'nobr', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'section', + 'select', + 'shadow', + 'small', + 'source', + 'spacer', + 'span', + 'strike', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'track', + 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr', +]; + +// SVG +export const svg = [ + 'svg', + 'a', + 'altglyph', + 'altglyphdef', + 'altglyphitem', + 'animatecolor', + 'animatemotion', + 'animatetransform', + 'audio', + 'canvas', + 'circle', + 'clippath', + 'defs', + 'desc', + 'ellipse', + 'filter', + 'font', + 'g', + 'glyph', + 'glyphref', + 'hkern', + 'image', + 'line', + 'lineargradient', + 'marker', + 'mask', + 'metadata', + 'mpath', + 'object', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialgradient', + 'rect', + 'stop', + 'style', + 'switch', + 'symbol', + 'text', + 'textpath', + 'title', + 'tref', + 'tspan', + 'video', + 'view', + 'vkern', +]; + +export const svgFilters = [ + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', +]; + +export const mathMl = [ + 'math', + 'menclose', + 'merror', + 'mfenced', + 'mfrac', + 'mglyph', + 'mi', + 'mlabeledtr', + 'mmultiscripts', + 'mn', + 'mo', + 'mover', + 'mpadded', + 'mphantom', + 'mroot', + 'mrow', + 'ms', + 'mspace', + 'msqrt', + 'mstyle', + 'msub', + 'msup', + 'msubsup', + 'mtable', + 'mtd', + 'mtext', + 'mtr', + 'munder', + 'munderover', +]; + +export const text = ['#text']; + +/** + * Anything in here will be executed as HTML tags from TechDocs. + * + * @note Be conscious about what you add here. It can affect the TechDocs experience. For + * some review before making a PR, reach out in the #docs-like-code channel in Discord first. + */ +export const TECHDOCS_ALLOWED_TAGS = [ + ...html, + ...svg, + ...svgFilters, + ...mathMl, + ...text, + 'iframe', +]; diff --git a/plugins/techdocs/src/test-utils/shadowDom.ts b/plugins/techdocs/src/test-utils/shadowDom.ts index ded6b6a860..556979217e 100644 --- a/plugins/techdocs/src/test-utils/shadowDom.ts +++ b/plugins/techdocs/src/test-utils/shadowDom.ts @@ -18,22 +18,36 @@ import transformer from '../reader/transformers'; import type { Transformer } from '../reader/transformers'; export type CreateTestShadowDomOptions = { - transformers: Transformer[]; + preTransformers: Transformer[]; + postTransformers: Transformer[]; }; export const createTestShadowDom = ( fixture: string, - opts: CreateTestShadowDomOptions = { transformers: [] }, + opts: CreateTestShadowDomOptions = { + preTransformers: [], + postTransformers: [], + }, ): ShadowRoot => { const divElement = document.createElement('div'); divElement.attachShadow({ mode: 'open' }); document.body.appendChild(divElement); - const domParser = new DOMParser().parseFromString(fixture, 'text/html'); - divElement.shadowRoot?.appendChild(domParser.documentElement); + // Transformers before the UI is rendered + let dom: Element | HTMLElement = new DOMParser().parseFromString( + fixture, + 'text/html', + ).documentElement; + if (opts.preTransformers) { + dom = transformer(dom, opts.preTransformers); + } - if (opts.transformers) { - transformer(divElement.shadowRoot!.children[0], opts.transformers); + // Mount the UI + divElement.shadowRoot?.appendChild(dom); + + // Transformers after the UI is rendered + if (opts.postTransformers) { + transformer(dom, opts.postTransformers); } return divElement.shadowRoot!; diff --git a/yarn.lock b/yarn.lock index 231e3ac884..8f5d9e8cec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3960,6 +3960,13 @@ "@types/node" "*" rollup "^0.63.4" +"@types/sanitize-html@^1.23.3": + version "1.23.3" + resolved "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-1.23.3.tgz#26527783aba3bf195ad8a3c3e51bd3713526fc0d" + integrity sha512-Isg8N0ifKdDq6/kaNlIcWfapDXxxquMSk2XC5THsOICRyOIhQGds95XH75/PL/g9mExi4bL8otIqJM/Wo96WxA== + dependencies: + htmlparser2 "^4.1.0" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -10031,7 +10038,7 @@ htmlparser2@^3.3.0: inherits "^2.0.1" readable-stream "^3.1.1" -htmlparser2@^4.0: +htmlparser2@^4.0, htmlparser2@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== @@ -16537,6 +16544,18 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" +sanitize-html@^1.27.0: + version "1.27.0" + resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.0.tgz#42104a2d59f1a48b616b5165ad5349824861e580" + integrity sha512-U1btucGeYVpg0GoK43jPpe/bDCV4cBOGuxzv5NBd0bOjyZdMKY0n98S/vNlO1wVwre0VCj8H3hbzE7gD2+RjKA== + dependencies: + chalk "^2.4.1" + htmlparser2 "^4.1.0" + lodash "^4.17.15" + postcss "^7.0.27" + srcset "^2.0.1" + xtend "^4.0.1" + sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -17154,6 +17173,11 @@ sqlite3@^4.2.0: nan "^2.12.1" node-pre-gyp "^0.11.0" +srcset@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac" + integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ== + ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34"