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 renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
......@@ -6,7 +6,7 @@ import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlRenderers = [renderHtml];
const htmlBlockRenderers = [renderBlockHtml];
const listRenderers = [renderKramdownList];
const paragraphRenderers = [renderIdentifierParagraph];
const textRenderers = [renderKramdownText, renderEmbeddedRubyText];
......@@ -32,9 +32,9 @@ const buildCustomHTMLRenderer = (
) => {
const defaults = {
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) {
const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
......@@ -47,7 +47,7 @@ const buildCustomHTMLRenderer = (
return executeRenderer(allListRenderers, node, context);
},
paragraph(node, context) {
const allParagraphRenderers = [...customRenderers.list, ...paragraphRenderers];
const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
return executeRenderer(allParagraphRenderers, node, context);
},
......
......@@ -4,25 +4,36 @@ const buildToken = (type, tagName, props) => {
const TAG_TYPES = {
block: 'div',
inline: 'span',
inline: 'a',
};
export const buildUneditableOpenTokens = (token, type = TAG_TYPES.block) => {
return [
buildToken('openTag', type, {
attributes: { contenteditable: false },
classNames: [
'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
],
}),
token,
];
// Open helpers (singular and multiple)
const buildUneditableOpenToken = (tagType = TAG_TYPES.block) =>
buildToken('openTag', tagType, {
attributes: { contenteditable: false },
classNames: [
'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed',
],
});
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) => {
return [token, buildUneditableCloseToken(type)];
export const buildUneditableTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
};
export const buildUneditableInlineTokens = token => {
......@@ -32,6 +43,19 @@ export const buildUneditableInlineTokens = token => {
];
};
export const buildUneditableTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
export const buildUneditableHtmlAsTextTokens = node => {
/*
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 }) => {
return type === 'htmlBlock';
};
const render = (_, { origin }) => buildUneditableTokens(origin());
const render = node => buildUneditableHtmlAsTextTokens(node);
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 {
buildUneditableOpenTokens,
buildUneditableCloseToken,
buildUneditableCloseTokens,
buildUneditableInlineTokens,
buildUneditableTokens,
buildUneditableInlineTokens,
buildUneditableHtmlAsTextTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import {
......@@ -12,6 +13,7 @@ import {
uneditableOpenTokens,
uneditableCloseToken,
uneditableCloseTokens,
uneditableBlockTokens,
uneditableInlineTokens,
uneditableTokens,
} from './mock_data';
......@@ -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', () => {
it('returns a 3-item array of tokens with the originInlineToken wrapped in the middle of inline tokens', () => {
const result = buildUneditableInlineTokens(originInlineToken);
......@@ -50,12 +61,20 @@ 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);
describe('buildUneditableHtmlAsTextTokens', () => {
it('returns a 3-item array of tokens with the htmlBlockNode wrapped as a text token in the middle of block tokens', () => {
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).toStrictEqual(uneditableTokens);
expect(result).toStrictEqual(uneditableBlockTokens);
});
});
});
......@@ -12,7 +12,7 @@ export const normalTextNode = buildMockTextNode('This is just normal text.');
// Token spec helpers
const buildUneditableOpenToken = type => {
const buildMockUneditableOpenToken = type => {
return {
type: 'openTag',
tagName: type,
......@@ -23,7 +23,7 @@ const buildUneditableOpenToken = type => {
};
};
const buildUneditableCloseToken = type => {
const buildMockUneditableCloseToken = type => {
return { type: 'closeTag', tagName: type };
};
......@@ -31,8 +31,8 @@ export const originToken = {
type: 'text',
content: '{:.no_toc .hidden-md .hidden-lg}',
};
export const uneditableCloseToken = buildUneditableCloseToken('div');
export const uneditableOpenTokens = [buildUneditableOpenToken('div'), originToken];
export const uneditableCloseToken = buildMockUneditableCloseToken('div');
export const uneditableOpenTokens = [buildMockUneditableOpenToken('div'), originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
......@@ -41,7 +41,17 @@ export const originInlineToken = {
content: '<i>Inline</i> content',
};
export const uneditableInlineTokens = [
buildUneditableOpenToken('span'),
buildMockUneditableOpenToken('a'),
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 { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
import { normalTextNode } from './mock_data';
const htmlLiteral = '<div><h1>Heading</h1><p>Paragraph.</p></div>';
const htmlBlockNode = {
firstChild: null,
literal: htmlLiteral,
literal: '<div><h1>Heading</h1><p>Paragraph.</p></div>',
type: 'htmlBlock',
};
......@@ -22,13 +21,18 @@ describe('Render HTML renderer', () => {
});
describe('render', () => {
it('should return uneditable tokens wrapping the origin token', () => {
const origin = jest.fn();
const context = { origin };
const htmlBlockNodeToMark = {
firstChild: null,
literal: '<div data-to-mark ></div>',
type: 'htmlBlock',
};
expect(renderer.render(htmlBlockNode, context)).toStrictEqual(
buildUneditableTokens(origin()),
);
it.each`
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