Merge pull request #1540 from spotify/mob/techdocs-sanitize-html
techdocs: sanitize html
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
<script src="../assets/javascripts/vendor.d710d30a.min.js"></script>
|
||||
`,
|
||||
{
|
||||
transformers: [
|
||||
preTransformers: [
|
||||
addBaseUrl({
|
||||
docStorageURL: DOC_STORAGE_URL,
|
||||
componentId: 'example-docs',
|
||||
path: 'examplepath',
|
||||
}),
|
||||
],
|
||||
postTransformers: [],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -108,13 +110,14 @@ describe('addBaseUrl', () => {
|
||||
<script src="../assets/javascripts/vendor.d710d30a.min.js"></script>
|
||||
`,
|
||||
{
|
||||
transformers: [
|
||||
preTransformers: [
|
||||
addBaseUrl({
|
||||
docStorageURL: DOC_STORAGE_URL,
|
||||
componentId: 'example-docs',
|
||||
path: 'examplepath/',
|
||||
}),
|
||||
],
|
||||
postTransformers: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ describe('modifyCss', () => {
|
||||
const shadowDom = createTestShadowDom(
|
||||
`<div class="md-typeset" style="font-size: 0.8em"></div>`,
|
||||
{
|
||||
transformers: [],
|
||||
preTransformers: [],
|
||||
postTransformers: [],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,13 +38,14 @@ describe('modifyCss', () => {
|
||||
const shadowDom = createTestShadowDom(
|
||||
`<div class="md-typeset" style="font-size: 1px"></div>`,
|
||||
{
|
||||
transformers: [
|
||||
preTransformers: [
|
||||
modifyCss({
|
||||
cssTransforms: {
|
||||
'.md-typeset': [{ 'font-size': '1em' }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
postTransformers: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -43,7 +43,8 @@ describe('rewriteDocLinks', () => {
|
||||
<a href="example-docs/example-page">Test Sub Page</a>
|
||||
`,
|
||||
{
|
||||
transformers: [rewriteDocLinks()],
|
||||
preTransformers: [rewriteDocLinks()],
|
||||
postTransformers: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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-*'],
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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!;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user