Commit d30d93cb authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '221078-v2-custom-renderer-html' into 'master'

Resolve "Display non-markdown content in the WYSIWYG mode of the SSE::HTML"

Closes #221078

See merge request gitlab-org/gitlab!36330
parents b0d5f0b3 d2fc5226
import renderHtml from './renderers/render_html'; import renderBlockHtml from './renderers/render_html_block';
import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text'; import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
...@@ -6,7 +6,7 @@ import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text'; ...@@ -6,7 +6,7 @@ import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlRenderers = [renderHtml]; const htmlBlockRenderers = [renderBlockHtml];
const listRenderers = [renderKramdownList]; const listRenderers = [renderKramdownList];
const paragraphRenderers = [renderIdentifierParagraph]; const paragraphRenderers = [renderIdentifierParagraph];
const textRenderers = [renderKramdownText, renderEmbeddedRubyText]; const textRenderers = [renderKramdownText, renderEmbeddedRubyText];
...@@ -32,9 +32,9 @@ const buildCustomHTMLRenderer = ( ...@@ -32,9 +32,9 @@ const buildCustomHTMLRenderer = (
) => { ) => {
const defaults = { const defaults = {
htmlBlock(node, context) { htmlBlock(node, context) {
const allHtmlRenderers = [...customRenderers.list, ...htmlRenderers]; const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers];
return executeRenderer(allHtmlRenderers, node, context); return executeRenderer(allHtmlBlockRenderers, node, context);
}, },
htmlInline(node, context) { htmlInline(node, context) {
const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers]; const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
...@@ -47,7 +47,7 @@ const buildCustomHTMLRenderer = ( ...@@ -47,7 +47,7 @@ const buildCustomHTMLRenderer = (
return executeRenderer(allListRenderers, node, context); return executeRenderer(allListRenderers, node, context);
}, },
paragraph(node, context) { paragraph(node, context) {
const allParagraphRenderers = [...customRenderers.list, ...paragraphRenderers]; const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
return executeRenderer(allParagraphRenderers, node, context); return executeRenderer(allParagraphRenderers, node, context);
}, },
......
...@@ -4,25 +4,36 @@ const buildToken = (type, tagName, props) => { ...@@ -4,25 +4,36 @@ const buildToken = (type, tagName, props) => {
const TAG_TYPES = { const TAG_TYPES = {
block: 'div', block: 'div',
inline: 'span', inline: 'a',
}; };
export const buildUneditableOpenTokens = (token, type = TAG_TYPES.block) => { // Open helpers (singular and multiple)
return [
buildToken('openTag', type, { const buildUneditableOpenToken = (tagType = TAG_TYPES.block) =>
attributes: { contenteditable: false }, buildToken('openTag', tagType, {
classNames: [ attributes: { contenteditable: false },
'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', classNames: [
], 'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
}), ],
token, });
];
export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => {
return [buildUneditableOpenToken(tagType), token];
};
// Close helpers (singular and multiple)
export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) =>
buildToken('closeTag', tagType);
export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => {
return [token, buildUneditableCloseToken(tagType)];
}; };
export const buildUneditableCloseToken = (type = TAG_TYPES.block) => buildToken('closeTag', type); // Complete helpers (open plus close)
export const buildUneditableCloseTokens = (token, type = TAG_TYPES.block) => { export const buildUneditableTokens = token => {
return [token, buildUneditableCloseToken(type)]; return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
}; };
export const buildUneditableInlineTokens = token => { export const buildUneditableInlineTokens = token => {
...@@ -32,6 +43,19 @@ export const buildUneditableInlineTokens = token => { ...@@ -32,6 +43,19 @@ export const buildUneditableInlineTokens = token => {
]; ];
}; };
export const buildUneditableTokens = token => { export const buildUneditableHtmlAsTextTokens = node => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; /*
Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain
nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want
to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html`
type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass `
to prevent their persistence within the `text` content as the user did not intend these as edits.
https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72
*/
const regex = / data-tomark-pass /gm;
const content = node.literal.replace(regex, '');
const htmlAsTextToken = buildToken('text', null, { content });
return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()];
}; };
import { buildUneditableTokens } from './build_uneditable_token'; import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
const canRender = ({ type }) => { const canRender = ({ type }) => {
return type === 'htmlBlock'; return type === 'htmlBlock';
}; };
const render = (_, { origin }) => buildUneditableTokens(origin()); const render = node => buildUneditableHtmlAsTextTokens(node);
export default { canRender, render }; export default { canRender, render };
---
title: Add a custom HTML renderer to the Static Site Editor for HTML block syntax
merge_request: 36330
author:
type: added
...@@ -2,8 +2,9 @@ import { ...@@ -2,8 +2,9 @@ import {
buildUneditableOpenTokens, buildUneditableOpenTokens,
buildUneditableCloseToken, buildUneditableCloseToken,
buildUneditableCloseTokens, buildUneditableCloseTokens,
buildUneditableInlineTokens,
buildUneditableTokens, buildUneditableTokens,
buildUneditableInlineTokens,
buildUneditableHtmlAsTextTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { import {
...@@ -12,6 +13,7 @@ import { ...@@ -12,6 +13,7 @@ import {
uneditableOpenTokens, uneditableOpenTokens,
uneditableCloseToken, uneditableCloseToken,
uneditableCloseTokens, uneditableCloseTokens,
uneditableBlockTokens,
uneditableInlineTokens, uneditableInlineTokens,
uneditableTokens, uneditableTokens,
} from './mock_data'; } from './mock_data';
...@@ -41,6 +43,15 @@ describe('Build Uneditable Token renderer helper', () => { ...@@ -41,6 +43,15 @@ describe('Build Uneditable Token renderer helper', () => {
}); });
}); });
describe('buildUneditableTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
const result = buildUneditableTokens(originToken);
expect(result).toHaveLength(3);
expect(result).toStrictEqual(uneditableTokens);
});
});
describe('buildUneditableInlineTokens', () => { describe('buildUneditableInlineTokens', () => {
it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => { it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => {
const result = buildUneditableInlineTokens(originInlineToken); const result = buildUneditableInlineTokens(originInlineToken);
...@@ -50,12 +61,20 @@ describe('Build Uneditable Token renderer helper', () => { ...@@ -50,12 +61,20 @@ describe('Build Uneditable Token renderer helper', () => {
}); });
}); });
describe('buildUneditableTokens', () => { describe('buildUneditableHtmlAsTextTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => { it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => {
const result = buildUneditableTokens(originToken); const htmlBlockNode = {
type: 'htmlBlock',
literal: '<div data-tomark-pass ><h1>Some header</h1><p>Some paragraph</p></div>',
};
const result = buildUneditableHtmlAsTextTokens(htmlBlockNode);
const { type, content } = result[1];
expect(type).toBe('text');
expect(content).not.toMatch(/ data-tomark-pass /);
expect(result).toHaveLength(3); expect(result).toHaveLength(3);
expect(result).toStrictEqual(uneditableTokens); expect(result).toStrictEqual(uneditableBlockTokens);
}); });
}); });
}); });
...@@ -12,7 +12,7 @@ export const normalTextNode = buildMockTextNode('This is just normal text.'); ...@@ -12,7 +12,7 @@ export const normalTextNode = buildMockTextNode('This is just normal text.');
// Token spec helpers // Token spec helpers
const buildUneditableOpenToken = type => { const buildMockUneditableOpenToken = type => {
return { return {
type: 'openTag', type: 'openTag',
tagName: type, tagName: type,
...@@ -23,7 +23,7 @@ const buildUneditableOpenToken = type => { ...@@ -23,7 +23,7 @@ const buildUneditableOpenToken = type => {
}; };
}; };
const buildUneditableCloseToken = type => { const buildMockUneditableCloseToken = type => {
return { type: 'closeTag', tagName: type }; return { type: 'closeTag', tagName: type };
}; };
...@@ -31,8 +31,8 @@ export const originToken = { ...@@ -31,8 +31,8 @@ export const originToken = {
type: 'text', type: 'text',
content: '{:.no_toc .hidden-md .hidden-lg}', content: '{:.no_toc .hidden-md .hidden-lg}',
}; };
export const uneditableCloseToken = buildUneditableCloseToken('div'); export const uneditableCloseToken = buildMockUneditableCloseToken('div');
export const uneditableOpenTokens = [buildUneditableOpenToken('div'), originToken]; export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken]; export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
...@@ -41,7 +41,17 @@ export const originInlineToken = { ...@@ -41,7 +41,17 @@ export const originInlineToken = {
content: '<i>Inline</i> content', content: '<i>Inline</i> content',
}; };
export const uneditableInlineTokens = [ export const uneditableInlineTokens = [
buildUneditableOpenToken('span'), buildMockUneditableOpenToken('a'),
originInlineToken, originInlineToken,
buildUneditableCloseToken('span'), buildMockUneditableCloseToken('a'),
];
export const uneditableBlockTokens = [
buildMockUneditableOpenToken('div'),
{
type: 'text',
tagName: null,
content: '<div><h1>Some header</h1><p>Some paragraph</p></div>',
},
buildMockUneditableCloseToken('div'),
]; ];
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html'; import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { normalTextNode } from './mock_data'; import { normalTextNode } from './mock_data';
const htmlLiteral = '<div><h1>Heading</h1><p>Paragraph.</p></div>';
const htmlBlockNode = { const htmlBlockNode = {
firstChild: null, firstChild: null,
literal: htmlLiteral, literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
type: 'htmlBlock', type: 'htmlBlock',
}; };
...@@ -22,13 +21,18 @@ describe('Render HTML renderer', () => { ...@@ -22,13 +21,18 @@ describe('Render HTML renderer', () => {
}); });
describe('render', () => { describe('render', () => {
it('should return uneditable tokens wrapping the origin token', () => { const htmlBlockNodeToMark = {
const origin = jest.fn(); firstChild: null,
const context = { origin }; literal: '<div data-to-mark ></div>',
type: 'htmlBlock',
};
expect(renderer.render(htmlBlockNode, context)).toStrictEqual( it.each`
buildUneditableTokens(origin()), node
); ${htmlBlockNode}
${htmlBlockNodeToMark}
`('should return uneditable tokens wrapping the $node as a token', ({ node }) => {
expect(renderer.render(node)).toStrictEqual(buildUneditableHtmlAsTextTokens(node));
}); });
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment