Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions demo/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {CSSProperties, useCallback, useEffect, useState} from 'react';

import sanitize from '@diplodoc/transform/lib/sanitize';
import {defaultOptions} from '@diplodoc/transform/lib/sanitize';
import {Button, DropdownMenu} from '@gravity-ui/uikit';
import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';

Expand All @@ -20,6 +20,7 @@ import {FoldingHeading} from '../src/extensions/yfm/FoldingHeading';
import {Math} from '../src/extensions/yfm/Math';
import {Mermaid} from '../src/extensions/yfm/Mermaid';
import {YfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock';
import {getSanitizeYfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock/utils';
import {cloneDeep} from '../src/lodash';
import type {FileUploadHandler} from '../src/utils/upload';
import {VERSION} from '../src/version';
Expand Down Expand Up @@ -164,7 +165,13 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
})
.use(YfmHtmlBlock, {
useConfig: useYfmHtmlBlockStyles,
sanitize,
sanitize: getSanitizeYfmHtmlBlock({options: defaultOptions}),
baseTarget: '_blank',
styles: {
body: {
margin: 0,
},
},
})
.use(FoldingHeading),
});
Expand Down
92 changes: 88 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
},
"devDependencies": {
"@diplodoc/folding-headings-extension": "0.1.0",
"@diplodoc/html-extension": "1.3.1",
"@diplodoc/html-extension": "1.3.3",
"@diplodoc/latex-extension": "1.0.3",
"@diplodoc/mermaid-extension": "1.2.1",
"@diplodoc/transform": "4.22.0",
Expand All @@ -223,6 +223,7 @@
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/rimraf": "3.0.2",
"@types/sanitize-html": "2.11.0",
"bem-cn-lite": "4.1.0",
"esbuild-sass-plugin": "2.15.0",
"eslint": "8.56.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ export class WYfmHtmlBlockNodeView implements NodeView {
getPos={this.getPos}
node={this.node}
onChange={this.onChange.bind(this)}
sanitize={this.options.sanitize}
useConfig={this.options.useConfig}
options={this.options}
view={this.view}
/>,
this.dom,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useEffect, useRef, useState} from 'react';

import {getStyles} from '@diplodoc/html-extension';
import type {IHTMLIFrameElementConfig} from '@diplodoc/html-extension/runtime';
import {Ellipsis as DotsIcon, Eye} from '@gravity-ui/icons';
import {Button, Icon, Label, Menu, Popup} from '@gravity-ui/uikit';
Expand All @@ -13,12 +14,13 @@ import {i18n} from '../../../../i18n/common';
import {useBooleanState} from '../../../../react-utils/hooks';
import {removeNode} from '../../../../utils/remove-node';
import {YfmHtmlBlockConsts} from '../YfmHtmlBlockSpecs/const';
import {YfmHtmlBlockOptions} from '../index';

import './YfmHtmlBlock.scss';

export const cnYfmHtmlBlock = cn('yfm-html-block');
export const cnHelper = cn('yfm-html-block-helper');

import './YfmHtmlBlock.scss';

const b = cnYfmHtmlBlock;

interface YfmHtmlBlockViewProps {
Expand Down Expand Up @@ -202,10 +204,9 @@ export const YfmHtmlBlockView: React.FC<{
getPos: () => number | undefined;
node: Node;
onChange: (attrs: {[YfmHtmlBlockConsts.NodeAttrs.srcdoc]: string}) => void;
sanitize?: (dirtyHtml: string) => string;
useConfig?: () => IHTMLIFrameElementConfig | undefined;
options: YfmHtmlBlockOptions;
view: EditorView;
}> = ({onChange, node, getPos, view, useConfig, sanitize}) => {
}> = ({onChange, node, getPos, view, options: {useConfig, sanitize, styles, baseTarget = '_'}}) => {
const [editing, setEditing, unsetEditing, toggleEditing] = useBooleanState(
Boolean(node.attrs[YfmHtmlBlockConsts.NodeAttrs.newCreated]),
);
Expand All @@ -232,7 +233,17 @@ export const YfmHtmlBlockView: React.FC<{
);
}

const dirtyHtml = node.attrs[YfmHtmlBlockConsts.NodeAttrs.srcdoc];
let dirtyHtml =
`<base target="${baseTarget}">` + node.attrs[YfmHtmlBlockConsts.NodeAttrs.srcdoc];

if (styles) {
const stylesContent =
typeof styles === 'string'
? `<link rel="stylesheet" href="${styles}" />`
: `<style>${getStyles(styles)}</style>`;
dirtyHtml = stylesContent + dirtyHtml;
}

const html = sanitize ? sanitize(dirtyHtml) : dirtyHtml;

return (
Expand Down
135 changes: 135 additions & 0 deletions src/extensions/yfm/YfmHtmlBlock/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {SanitizeOptions} from '@diplodoc/transform/lib/sanitize';

import {getSanitizeYfmHtmlBlock, getYfmHtmlBlockOptions} from './utils'; // update the path accordingly

// remove all whitespaces and newline characters
const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim();

describe('sanitize options functions', () => {
const defaultOptions: SanitizeOptions = {
allowedTags: ['b', 'i', 'strong', 'em'],
allowedAttributes: {
a: ['href', 'name', 'target'],
},
cssWhiteList: {
color: true,
'font-weight': true,
},
};

it('should merge additional tags into default options', () => {
const options = getYfmHtmlBlockOptions(defaultOptions);
expect(options.allowedTags).toEqual(expect.arrayContaining(['link', 'base', 'style']));
});

it('should merge additional attributes into default options', () => {
const options = getYfmHtmlBlockOptions(defaultOptions);
expect(options.allowedAttributes).toEqual({
...defaultOptions.allowedAttributes,
link: ['rel', 'href'],
base: ['target'],
style: [],
});
});

it('should merge additional css properties into default options', () => {
const options = getYfmHtmlBlockOptions(defaultOptions);
expect(options.cssWhiteList).toEqual({
...defaultOptions.cssWhiteList,
'align-content': true,
'align-items': true,
'align-self': true,
color: true,
'column-count': true,
'column-fill': true,
'column-gap': true,
'column-rule': true,
'column-rule-color': true,
'column-rule-style': true,
'column-rule-width': true,
'column-span': true,
'column-width': true,
columns: true,
flex: true,
'flex-basis': true,
'flex-direction': true,
'flex-flow': true,
'flex-grow': true,
'flex-shrink': true,
'flex-wrap': true,
'font-weight': true,
gap: true,
grid: true,
'grid-area': true,
'grid-auto-columns': true,
'grid-auto-flow': true,
'grid-auto-rows': true,
'grid-column': true,
'grid-column-end': true,
'grid-column-start': true,
'grid-row': true,
'grid-row-end': true,
'grid-row-start': true,
'grid-template': true,
'grid-template-areas': true,
'grid-template-columns': true,
'grid-template-rows': true,
'justify-content': true,
'justify-items': true,
'justify-self': true,
'line-height': true,
'object-fit': true,
'object-position': true,
order: true,
orphans: true,
'row-gap': true,
});
});
});

describe('sanitize HTML function', () => {
const options: SanitizeOptions = {
allowedTags: ['b', 'i', 'strong', 'em'],
allowedAttributes: {
a: ['href', 'name', 'target'],
},
cssWhiteList: {
color: true,
'font-weight': true,
},
};

it('should sanitize HTML content with additional options', () => {
const htmlContent = `
<b>Bold</b>
<i>Italic</i>
<link href="styles.css" rel="stylesheet" />
<base target="_blank" />
<style>.example { flex: 1; columns: 1; }</style>
`;

const sanitizeYfmHtmlBlock = getSanitizeYfmHtmlBlock({options});
const sanitizedContent = normalizeWhitespace(sanitizeYfmHtmlBlock(htmlContent));

expect(sanitizedContent).toContain('<link href="styles.css" rel="stylesheet" />');
expect(sanitizedContent).toContain('<base target="_blank" />');
expect(sanitizedContent).toContain(
normalizeWhitespace('<style>.example { flex: 1; columns: 1; }</style>'),
);
});

it('should sanitize HTML content using a custom sanitize function', () => {
// example of custom sanitize logic
const customSanitize = (html: string, _?: SanitizeOptions): string => {
return html.replace(/<style.*<\/style>/, '<style></style>');
};

const htmlContent = '<style>.example { flex: 1; columns: 1; }</style>';
const expectedSanitizedContent = '<style></style>';

const sanitizeYfmHtmlBlock = getSanitizeYfmHtmlBlock({options, sanitize: customSanitize});
const sanitizedContent = sanitizeYfmHtmlBlock(htmlContent);

expect(sanitizedContent).toEqual(expectedSanitizedContent);
});
});
Loading