diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js index a9c5d442f62e6102038fed61ec3c46f7770d8093..108c60c3edb3e87260b5f3a0a9f724b7786b3338 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js @@ -1,17 +1,19 @@ import { union, mapValues } from 'lodash'; import renderBlockHtml from './renderers/render_html_block'; -import renderKramdownList from './renderers/render_kramdown_list'; -import renderKramdownText from './renderers/render_kramdown_text'; +import renderHeading from './renderers/render_heading'; import renderIdentifierInstanceText from './renderers/render_identifier_instance_text'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; import renderSoftbreak from './renderers/render_softbreak'; +import renderAttributeDefinition from './renderers/render_attribute_definition'; +import renderListItem from './renderers/render_list_item'; const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; const htmlBlockRenderers = [renderBlockHtml]; -const listRenderers = [renderKramdownList]; -const paragraphRenderers = [renderIdentifierParagraph]; -const textRenderers = [renderKramdownText, renderIdentifierInstanceText]; +const headingRenderers = [renderHeading]; +const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml]; +const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition]; +const listItemRenderers = [renderListItem]; const softbreakRenderers = [renderSoftbreak]; const executeRenderer = (renderers, node, context) => { @@ -25,7 +27,8 @@ const buildCustomHTMLRenderer = customRenderers => { ...customRenderers, htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock), htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline), - list: union(listRenderers, customRenderers?.list), + heading: union(headingRenderers, customRenderers?.heading), + item: union(listItemRenderers, customRenderers?.listItem), paragraph: union(paragraphRenderers, customRenderers?.paragraph), text: union(textRenderers, customRenderers?.text), softbreak: union(softbreakRenderers, customRenderers?.softbreak), diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js index 868ede9426ea6795b81c15b4788c5c208bfa2a87..74c7a3853bf627a15217c56b6a0f352e9d9f2f84 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js @@ -28,6 +28,7 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => const orderedListItemNode = 'OL LI'; const emphasisNode = 'EM, I'; const strongNode = 'STRONG, B'; + const headingNode = 'H1, H2, H3, H4, H5, H6'; return { TEXT_NODE(node) { @@ -63,8 +64,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => }, [unorderedListItemNode](node, subContent) { const baseResult = baseRenderer.convert(node, subContent); + const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); + const { attributeDefinition } = node.dataset; - return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); + return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted; }, [orderedListItemNode](node, subContent) { const baseResult = baseRenderer.convert(node, subContent); @@ -82,6 +85,12 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax); }, + [headingNode](node, subContent) { + const result = baseRenderer.convert(node, subContent); + const { attributeDefinition } = node.dataset; + + return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result; + }, }; }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js new file mode 100644 index 0000000000000000000000000000000000000000..bd419447a4898c9dd3801b2b91099c0fbbc4b702 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js @@ -0,0 +1,7 @@ +import { isAttributeDefinition } from './render_utils'; + +const canRender = ({ literal }) => isAttributeDefinition(literal); + +const render = () => ({ type: 'html', content: '' }); + +export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js new file mode 100644 index 0000000000000000000000000000000000000000..71026fd0d65fa2e3eabc6c92fb426757d7607464 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js @@ -0,0 +1,6 @@ +import { + renderWithAttributeDefinitions as render, + willAlwaysRender as canRender, +} from './render_utils'; + +export default { render, canRender }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js deleted file mode 100644 index 949ca0e5c2a38556d1ce1be84106daf87428bbc6..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js +++ /dev/null @@ -1,24 +0,0 @@ -import { renderUneditableBranch as render } from './render_utils'; - -const isKramdownTOC = ({ type, literal }) => type === 'text' && literal === 'TOC'; - -const canRender = node => { - let targetNode = node; - while (targetNode !== null) { - const { firstChild } = targetNode; - const isLeaf = firstChild === null; - if (isLeaf) { - if (isKramdownTOC(targetNode)) { - return true; - } - - break; - } - - targetNode = targetNode.firstChild; - } - - return false; -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js deleted file mode 100644 index 0551894918ce3e1c1bae0e0fc8b3de77a02cae1e..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js +++ /dev/null @@ -1,9 +0,0 @@ -import { renderUneditableLeaf as render } from './render_utils'; - -const kramdownRegex = /(^{:.+}$)/; - -const canRender = ({ literal }) => { - return kramdownRegex.test(literal); -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js new file mode 100644 index 0000000000000000000000000000000000000000..71026fd0d65fa2e3eabc6c92fb426757d7607464 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js @@ -0,0 +1,6 @@ +import { + renderWithAttributeDefinitions as render, + willAlwaysRender as canRender, +} from './render_utils'; + +export default { render, canRender }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js index cec6491557bbda2a50730347393a94258ca0be24..4cba2c70486d491370e61361ca99b838610b8808 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js @@ -8,3 +8,31 @@ export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockToken export const renderUneditableBranch = (_, { entering, origin }) => entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken(); + +const attributeDefinitionRegexp = /(^{:.+}$)/; + +export const isAttributeDefinition = text => attributeDefinitionRegexp.test(text); + +const findAttributeDefinition = node => { + const literal = + node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items; + + return isAttributeDefinition(literal) ? literal : null; +}; + +export const renderWithAttributeDefinitions = (node, { origin }) => { + const attributes = findAttributeDefinition(node); + const token = origin(); + + if (token.type === 'openTag' && attributes) { + Object.assign(token, { + attributes: { + 'data-attribute-definition': attributes, + }, + }); + } + + return token; +}; + +export const willAlwaysRender = () => true; diff --git a/changelogs/unreleased/render-attribute-definitions.yml b/changelogs/unreleased/render-attribute-definitions.yml new file mode 100644 index 0000000000000000000000000000000000000000..774f1d90d0d0fc48b4fc1edd66d08069bef9372b --- /dev/null +++ b/changelogs/unreleased/render-attribute-definitions.yml @@ -0,0 +1,5 @@ +--- +title: Render markdown attribute definitions as tooltips +merge_request: 40541 +author: +type: changed diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js index cafe53e6bb2cec49045d6880601d8308fae46e66..a823d04024d4aace2d4d6faf41453cc0c23043d0 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js @@ -5,8 +5,13 @@ describe('Build Custom Renderer Service', () => { it('should return an object with the default renderer functions when lacking arguments', () => { expect(buildCustomHTMLRenderer()).toEqual( expect.objectContaining({ - list: expect.any(Function), + htmlBlock: expect.any(Function), + htmlInline: expect.any(Function), + heading: expect.any(Function), + item: expect.any(Function), + paragraph: expect.any(Function), text: expect.any(Function), + softbreak: expect.any(Function), }), ); }); @@ -20,8 +25,6 @@ describe('Build Custom Renderer Service', () => { expect(buildCustomHTMLRenderer(customRenderers)).toEqual( expect.objectContaining({ html: expect.any(Function), - list: expect.any(Function), - text: expect.any(Function), }), ); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js index a90d3528d60682f64bcd2d76c8a46e17c8605d97..812aa2184ec8774449f9d4c42bf8e78cb398f1bb 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js @@ -1,9 +1,10 @@ import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer'; +import { attributeDefinition } from './renderers/mock_data'; -describe('HTMLToMarkdownRenderer', () => { +describe('rich_content_editor/services/html_to_markdown_renderer', () => { let baseRenderer; let htmlToMarkdownRenderer; - const NODE = { nodeValue: 'mock_node' }; + let fakeNode; beforeEach(() => { baseRenderer = { @@ -12,14 +13,16 @@ describe('HTMLToMarkdownRenderer', () => { getSpaceControlled: jest.fn(input => `space controlled ${input}`), convert: jest.fn(), }; + + fakeNode = { nodeValue: 'mock_node', dataset: {} }; }); describe('TEXT_NODE visitor', () => { it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => { htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); - expect(htmlToMarkdownRenderer.TEXT_NODE(NODE)).toBe( - `space controlled trimmed space collapsed ${NODE.nodeValue}`, + expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe( + `space controlled trimmed space collapsed ${fakeNode.nodeValue}`, ); }); }); @@ -43,8 +46,8 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(list); - expect(htmlToMarkdownRenderer['LI OL, LI UL'](NODE, list)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list); + expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list); }); }); @@ -62,10 +65,21 @@ describe('HTMLToMarkdownRenderer', () => { }); baseRenderer.convert.mockReturnValueOnce(listItem); - expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem); + expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem); }, ); + + it('detects attribute definitions and attaches them to the list item', () => { + const listItem = '- list item'; + const result = `${listItem}\n${attributeDefinition}\n`; + + fakeNode.dataset.attributeDefinition = attributeDefinition; + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); + baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`); + + expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result); + }); }); describe('OL LI visitor', () => { @@ -85,8 +99,8 @@ describe('HTMLToMarkdownRenderer', () => { }); baseRenderer.convert.mockReturnValueOnce(listItem); - expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent); + expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent); }, ); }); @@ -105,8 +119,8 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(input); - expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); }, ); }); @@ -125,9 +139,22 @@ describe('HTMLToMarkdownRenderer', () => { baseRenderer.convert.mockReturnValueOnce(input); - expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result); - expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input); + expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result); + expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input); }, ); }); + + describe('H1, H2, H3, H4, H5, H6 visitor', () => { + it('detects attribute definitions and attaches them to the heading', () => { + const heading = 'heading text'; + const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`; + + fakeNode.dataset.attributeDefinition = attributeDefinition; + htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer); + baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`); + + expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js index 660c21281fde574f727174b8cd6f52af8f29d018..749a66d6833648104b8abb4f3b51f4a1ed4a648a 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js @@ -56,3 +56,5 @@ export const uneditableBlockTokens = [ }, buildMockUneditableCloseToken('div'), ]; + +export const attributeDefinition = '{:.no_toc .hidden-md .hidden-lg}'; diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..69fd9a67a215fd6e25f7bb6b82854667aba9fe7d --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js @@ -0,0 +1,25 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition'; +import { attributeDefinition } from './mock_data'; + +describe('rich_content_editor/renderers/render_attribute_definition', () => { + describe('canRender', () => { + it.each` + input | result + ${{ literal: attributeDefinition }} | ${true} + ${{ literal: `FOO${attributeDefinition}` }} | ${false} + ${{ literal: `${attributeDefinition}BAR` }} | ${false} + ${{ literal: 'foobar' }} | ${false} + `('returns $result when input is $input', ({ input, result }) => { + expect(renderer.canRender(input)).toBe(result); + }); + }); + + describe('render', () => { + it('returns an empty HTML comment', () => { + expect(renderer.render()).toEqual({ + type: 'html', + content: '', + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..76abc1ec3d80484ef98163d8c7314120c9a04fee --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js @@ -0,0 +1,12 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_heading'; +import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; + +describe('rich_content_editor/renderers/render_heading', () => { + it('canRender delegates to renderUtils.willAlwaysRender', () => { + expect(renderer.canRender).toBe(renderUtils.willAlwaysRender); + }); + + it('render delegates to renderUtils.renderWithAttributeDefinitions', () => { + expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js deleted file mode 100644 index 7d427108ba6cfa4393fe2d6be374e9e285a2d60f..0000000000000000000000000000000000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list'; -import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -import { buildMockTextNode } from './mock_data'; - -const buildMockListNode = literal => { - return { - firstChild: { - firstChild: { - firstChild: buildMockTextNode(literal), - type: 'paragraph', - }, - type: 'item', - }, - type: 'list', - }; -}; - -const normalListNode = buildMockListNode('Just another bullet point'); -const kramdownListNode = buildMockListNode('TOC'); - -describe('Render Kramdown List renderer', () => { - describe('canRender', () => { - it('should return true when the argument is a special kramdown TOC ordered/unordered list', () => { - expect(renderer.canRender(kramdownListNode)).toBe(true); - }); - - it('should return false when the argument is a normal ordered/unordered list', () => { - expect(renderer.canRender(normalListNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should delegate rendering to the renderUneditableBranch util', () => { - expect(renderer.render).toBe(renderUneditableBranch); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js deleted file mode 100644 index 1d2d152ffc3d083b8c432bbac81e89e24016e5ea..0000000000000000000000000000000000000000 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text'; -import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; - -import { buildMockTextNode, normalTextNode } from './mock_data'; - -const kramdownTextNode = buildMockTextNode('{:toc}'); - -describe('Render Kramdown Text renderer', () => { - describe('canRender', () => { - it('should return true when the argument `literal` has kramdown syntax', () => { - expect(renderer.canRender(kramdownTextNode)).toBe(true); - }); - - it('should return false when the argument `literal` lacks kramdown syntax', () => { - expect(renderer.canRender(normalTextNode)).toBe(false); - }); - }); - - describe('render', () => { - it('should delegate rendering to the renderUneditableLeaf util', () => { - expect(renderer.render).toBe(renderUneditableLeaf); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c1ab700535b603918a4d4ba635ae7baff799db41 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js @@ -0,0 +1,12 @@ +import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_list_item'; +import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; + +describe('rich_content_editor/renderers/render_list_item', () => { + it('canRender delegates to renderUtils.willAlwaysRender', () => { + expect(renderer.canRender).toBe(renderUtils.willAlwaysRender); + }); + + it('render delegates to renderUtils.renderWithAttributeDefinitions', () => { + expect(renderer.render).toBe(renderUtils.renderWithAttributeDefinitions); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js index 92435b3e4e3d67518a38a9feec12c7a76c5dc78e..774f830f421ae60d055013cb6a7025b54e21c971 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js @@ -1,6 +1,8 @@ import { renderUneditableLeaf, renderUneditableBranch, + renderWithAttributeDefinitions, + willAlwaysRender, } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils'; import { @@ -8,9 +10,9 @@ import { buildUneditableOpenTokens, } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; -import { originToken, uneditableCloseToken } from './mock_data'; +import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data'; -describe('Render utils', () => { +describe('rich_content_editor/renderers/render_utils', () => { describe('renderUneditableLeaf', () => { it('should return uneditable block tokens around an origin token', () => { const context = { origin: jest.fn().mockReturnValueOnce(originToken) }; @@ -41,4 +43,68 @@ describe('Render utils', () => { expect(result).toStrictEqual(uneditableCloseToken); }); }); + + describe('willAlwaysRender', () => { + it('always returns true', () => { + expect(willAlwaysRender()).toBe(true); + }); + }); + + describe('renderWithAttributeDefinitions', () => { + let openTagToken; + let closeTagToken; + let node; + const attributes = { + 'data-attribute-definition': attributeDefinition, + }; + + beforeEach(() => { + openTagToken = { type: 'openTag' }; + closeTagToken = { type: 'closeTag' }; + node = { + next: { + firstChild: { + literal: attributeDefinition, + }, + }, + }; + }); + + describe('when token type is openTag', () => { + it('attaches attributes when attributes exist in the node’s next sibling', () => { + const context = { origin: () => openTagToken }; + + expect(renderWithAttributeDefinitions(node, context)).toEqual({ + ...openTagToken, + attributes, + }); + }); + + it('attaches attributes when attributes exist in the node’s children', () => { + const context = { origin: () => openTagToken }; + node = { + firstChild: { + firstChild: { + next: { + next: { + literal: attributeDefinition, + }, + }, + }, + }, + }; + + expect(renderWithAttributeDefinitions(node, context)).toEqual({ + ...openTagToken, + attributes, + }); + }); + }); + + it('does not attach attributes when token type is "closeTag"', () => { + const context = { origin: () => closeTagToken }; + + expect(renderWithAttributeDefinitions({}, context)).toBe(closeTagToken); + }); + }); });