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: '<!-- sse-attribute-definition -->' });
+
+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: '<!-- sse-attribute-definition -->',
+      });
+    });
+  });
+});
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);
+    });
+  });
 });