diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js
index f277508f628a849c770fedb7d9b82617c21ab7fa..4af9dc8e40559628134f32f73b4491f078d2ef57 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants.js
@@ -45,3 +45,7 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
 export const LOADING_CONTENT_EVENT = 'loadingContent';
 export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
 export const LOADING_ERROR_EVENT = 'loadingError';
+
+export const PARSE_HTML_PRIORITY_LOWEST = 1;
+export const PARSE_HTML_PRIORITY_DEFAULT = 50;
+export const PARSE_HTML_PRIORITY_HIGHEST = 100;
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
new file mode 100644
index 0000000000000000000000000000000000000000..e312775e75c8137bf422b96d14c69513aac5d941
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -0,0 +1,66 @@
+import { Mark, mergeAttributes, markInputRule } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+const marks = [
+  'ins',
+  'abbr',
+  'bdo',
+  'cite',
+  'dfn',
+  'mark',
+  'small',
+  'span',
+  'time',
+  'kbd',
+  'q',
+  'samp',
+  'var',
+  'ruby',
+  'rp',
+  'rt',
+];
+
+const attrs = {
+  time: ['datetime'],
+  abbr: ['title'],
+  span: ['dir'],
+  bdo: ['dir'],
+};
+
+export default marks.map((name) =>
+  Mark.create({
+    name,
+
+    inclusive: false,
+
+    defaultOptions: {
+      HTMLAttributes: {},
+    },
+
+    addAttributes() {
+      return (attrs[name] || []).reduce(
+        (acc, attr) => ({
+          ...acc,
+          [attr]: {
+            default: null,
+            parseHTML: (element) => ({ [attr]: element.getAttribute(attr) }),
+          },
+        }),
+        {},
+      );
+    },
+
+    parseHTML() {
+      return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
+    },
+
+    renderHTML({ HTMLAttributes }) {
+      return [name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
+    },
+
+    addInputRules() {
+      return [markInputRule(markInputRegex(name), this.type, extractMarkAttributesFromMatch)];
+    },
+  }),
+);
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 81b974af91419246c1dbe295dd896e3aaf1e3ae9..afd23b55a63079150030ad35ef7dfb04b2b173e0 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,6 +1,7 @@
 import { Image } from '@tiptap/extension-image';
 import { VueNodeViewRenderer } from '@tiptap/vue-2';
 import ImageWrapper from '../components/wrappers/image.vue';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
 
 const resolveImageEl = (element) =>
   element.nodeName === 'IMG' ? element : element.querySelector('img');
@@ -65,7 +66,7 @@ export default Image.extend({
   parseHTML() {
     return [
       {
-        priority: 100,
+        priority: PARSE_HTML_PRIORITY_HIGHEST,
         tag: 'a.no-attachment-icon',
       },
       {
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index 5f4484af9c8be3d0e228b438099748dd2eb35dce..fa5429ae0f48d104501eaab4741e6c5b337f46a7 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -1,4 +1,10 @@
 import { Node } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+
+const getAnchor = (element) => {
+  if (element.nodeName === 'A') return element;
+  return element.querySelector('a');
+};
 
 export default Node.create({
   name: 'reference',
@@ -15,7 +21,7 @@ export default Node.create({
         default: null,
         parseHTML: (element) => {
           return {
-            className: element.className,
+            className: getAnchor(element).className,
           };
         },
       },
@@ -23,7 +29,7 @@ export default Node.create({
         default: null,
         parseHTML: (element) => {
           return {
-            referenceType: element.dataset.referenceType,
+            referenceType: getAnchor(element).dataset.referenceType,
           };
         },
       },
@@ -31,7 +37,7 @@ export default Node.create({
         default: null,
         parseHTML: (element) => {
           return {
-            originalText: element.dataset.original,
+            originalText: getAnchor(element).dataset.original,
           };
         },
       },
@@ -39,7 +45,7 @@ export default Node.create({
         default: null,
         parseHTML: (element) => {
           return {
-            href: element.getAttribute('href'),
+            href: getAnchor(element).getAttribute('href'),
           };
         },
       },
@@ -47,7 +53,7 @@ export default Node.create({
         default: null,
         parseHTML: (element) => {
           return {
-            text: element.textContent,
+            text: getAnchor(element).textContent,
           };
         },
       },
@@ -58,7 +64,10 @@ export default Node.create({
     return [
       {
         tag: 'a.gfm:not([data-link=true])',
-        priority: 51,
+        priority: PARSE_HTML_PRIORITY_HIGHEST,
+      },
+      {
+        tag: 'span.gl-label',
       },
     ];
   },
diff --git a/app/assets/javascripts/content_editor/extensions/subscript.js b/app/assets/javascripts/content_editor/extensions/subscript.js
index 4bf89796efef42f7fded2f818bf1efbd33dc047d..d0766f42308f4d090a28731b56b959637fd0ee8d 100644
--- a/app/assets/javascripts/content_editor/extensions/subscript.js
+++ b/app/assets/javascripts/content_editor/extensions/subscript.js
@@ -1 +1,9 @@
-export { Subscript as default } from '@tiptap/extension-subscript';
+import { markInputRule } from '@tiptap/core';
+import { Subscript } from '@tiptap/extension-subscript';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+export default Subscript.extend({
+  addInputRules() {
+    return [markInputRule(markInputRegex('sub'), this.type, extractMarkAttributesFromMatch)];
+  },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/superscript.js b/app/assets/javascripts/content_editor/extensions/superscript.js
index 3eb7d86d90d328ece785cb283a301d0b8d3efb30..6cd814977ea9f36baf62187668eb77b67f71f940 100644
--- a/app/assets/javascripts/content_editor/extensions/superscript.js
+++ b/app/assets/javascripts/content_editor/extensions/superscript.js
@@ -1 +1,9 @@
-export { Superscript as default } from '@tiptap/extension-superscript';
+import { markInputRule } from '@tiptap/core';
+import { Superscript } from '@tiptap/extension-superscript';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+export default Superscript.extend({
+  addInputRules() {
+    return [markInputRule(markInputRegex('sup'), this.type, extractMarkAttributesFromMatch)];
+  },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/task_item.js b/app/assets/javascripts/content_editor/extensions/task_item.js
index 6163c0e043ba46b32d5629a274c04b2d2b5ed6d4..d586478358d0d4ea1c9d6c16b55cfa36e57ee1b2 100644
--- a/app/assets/javascripts/content_editor/extensions/task_item.js
+++ b/app/assets/javascripts/content_editor/extensions/task_item.js
@@ -1,4 +1,5 @@
 import { TaskItem } from '@tiptap/extension-task-item';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
 
 export default TaskItem.extend({
   defaultOptions: {
@@ -26,7 +27,7 @@ export default TaskItem.extend({
     return [
       {
         tag: 'li.task-list-item',
-        priority: 100,
+        priority: PARSE_HTML_PRIORITY_HIGHEST,
       },
     ];
   },
diff --git a/app/assets/javascripts/content_editor/extensions/task_list.js b/app/assets/javascripts/content_editor/extensions/task_list.js
index b7f6c857bc7a9153e56a3fbcdd2d055096ffdfff..72806c944fbc4ff8edaa1cdea47789e16096a3e7 100644
--- a/app/assets/javascripts/content_editor/extensions/task_list.js
+++ b/app/assets/javascripts/content_editor/extensions/task_list.js
@@ -1,5 +1,6 @@
 import { mergeAttributes } from '@tiptap/core';
 import { TaskList } from '@tiptap/extension-task-list';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
 
 export default TaskList.extend({
   addAttributes() {
@@ -19,7 +20,7 @@ export default TaskList.extend({
     return [
       {
         tag: '.task-list',
-        priority: 100,
+        priority: PARSE_HTML_PRIORITY_HIGHEST,
       },
     ];
   },
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 8997960203acd0da27af2413b19e198af7738487..67d5a00b6c620e161d06f116449b8362ffa7472d 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -15,6 +15,7 @@ import HardBreak from '../extensions/hard_break';
 import Heading from '../extensions/heading';
 import History from '../extensions/history';
 import HorizontalRule from '../extensions/horizontal_rule';
+import HTMLMarks from '../extensions/html_marks';
 import Image from '../extensions/image';
 import InlineDiff from '../extensions/inline_diff';
 import Italic from '../extensions/italic';
@@ -75,6 +76,7 @@ export const createContentEditor = ({
     Heading,
     History,
     HorizontalRule,
+    ...HTMLMarks,
     Image,
     InlineDiff,
     Italic,
diff --git a/app/assets/javascripts/content_editor/services/mark_utils.js b/app/assets/javascripts/content_editor/services/mark_utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..6ccfed7810a1bc8eb42b969b017c5edcf9d08e47
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/mark_utils.js
@@ -0,0 +1,17 @@
+export const markInputRegex = (tag) =>
+  new RegExp(`(<(${tag})((?: \\w+=".+?")+)?>([^<]+)</${tag}>)$`, 'gm');
+
+export const extractMarkAttributesFromMatch = ([, , , attrsString]) => {
+  const attrRegex = /(\w+)="(.+?)"/g;
+  const attrs = {};
+
+  let key;
+  let value;
+
+  do {
+    [, key, value] = attrRegex.exec(attrsString) || [];
+    if (key) attrs[key] = value;
+  } while (key);
+
+  return attrs;
+};
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 73d14f93e79b694f485ab616a9876480a8602328..d1f7e88b1db4bd99887349e14b8e9ccfb1ddee1c 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -12,6 +12,7 @@ import Emoji from '../extensions/emoji';
 import HardBreak from '../extensions/hard_break';
 import Heading from '../extensions/heading';
 import HorizontalRule from '../extensions/horizontal_rule';
+import HTMLMarks from '../extensions/html_marks';
 import Image from '../extensions/image';
 import InlineDiff from '../extensions/inline_diff';
 import Italic from '../extensions/italic';
@@ -35,6 +36,8 @@ import {
   renderTable,
   renderTableCell,
   renderTableRow,
+  openTag,
+  closeTag,
 } from './serialization_helpers';
 
 const defaultSerializerConfig = {
@@ -70,6 +73,19 @@ const defaultSerializerConfig = {
       mixable: true,
       expelEnclosingWhitespace: true,
     },
+    ...HTMLMarks.reduce(
+      (acc, { name }) => ({
+        ...acc,
+        [name]: {
+          mixable: true,
+          open(state, node) {
+            return openTag(name, node.attrs);
+          },
+          close: closeTag(name),
+        },
+      }),
+      {},
+    ),
   },
 
   nodes: {
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 909ab3dbd6801ad07852815172c6a0877e2c45fb..e1d0388227b38d19b29f47d454efe3893ed6a555 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -80,21 +80,30 @@ function shouldRenderHTMLTable(table) {
   return true;
 }
 
-function openTag(state, tagName, attrs) {
+function htmlEncode(str = '') {
+  return str
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/'/g, '&#39;')
+    .replace(/"/g, '&#34;');
+}
+
+export function openTag(tagName, attrs) {
   let str = `<${tagName}`;
 
   str += Object.entries(attrs || {})
     .map(([key, value]) => {
       if (defaultAttrs[tagName]?.[key] === value) return '';
 
-      return ` ${key}=${state.quote(value?.toString() || '')}`;
+      return ` ${key}="${htmlEncode(value?.toString())}"`;
     })
     .join('');
 
   return `${str}>`;
 }
 
-function closeTag(state, tagName) {
+export function closeTag(tagName) {
   return `</${tagName}>`;
 }
 
@@ -131,11 +140,11 @@ function unsetIsInBlockTable(table) {
 
 function renderTagOpen(state, tagName, attrs) {
   state.ensureNewLine();
-  state.write(openTag(state, tagName, attrs));
+  state.write(openTag(tagName, attrs));
 }
 
 function renderTagClose(state, tagName, insertNewline = true) {
-  state.write(closeTag(state, tagName));
+  state.write(closeTag(tagName));
   if (insertNewline) state.ensureNewLine();
 }
 
diff --git a/spec/frontend/content_editor/services/mark_utils_spec.js b/spec/frontend/content_editor/services/mark_utils_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bbfb8f26f99c0b98c7b8df0c03fdabcc280a87af
--- /dev/null
+++ b/spec/frontend/content_editor/services/mark_utils_spec.js
@@ -0,0 +1,38 @@
+import {
+  markInputRegex,
+  extractMarkAttributesFromMatch,
+} from '~/content_editor/services/mark_utils';
+
+describe('content_editor/services/mark_utils', () => {
+  describe.each`
+    tag       | input                                                               | matches
+    ${'tag'}  | ${'<tag>hello</tag>'}                                               | ${true}
+    ${'tag'}  | ${'<tag title="tooltip">hello</tag>'}                               | ${true}
+    ${'kbd'}  | ${'Hold <kbd>Ctrl</kbd>'}                                           | ${true}
+    ${'time'} | ${'Lets meet at <time title="today" datetime="20:00">20:00</time>'} | ${true}
+    ${'tag'}  | ${'<tag width=30 height=30>attrs not quoted</tag>'}                 | ${false}
+    ${'tag'}  | ${"<tag title='abc'>single quote attrs not supported</tag>"}        | ${false}
+    ${'tag'}  | ${'<tag title>attr has no value</tag>'}                             | ${false}
+    ${'tag'}  | ${'<tag>tag opened but not closed'}                                 | ${false}
+    ${'tag'}  | ${'</tag>tag closed before opened<tag>'}                            | ${false}
+  `('inputRegex("$tag")', ({ tag, input, matches }) => {
+    it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
+      const match = markInputRegex(tag).test(input);
+
+      expect(match).toBe(matches);
+    });
+  });
+
+  describe.each`
+    tag       | input                                                                             | attrs
+    ${'kbd'}  | ${'Hold <kbd>Ctrl</kbd>'}                                                         | ${{}}
+    ${'tag'}  | ${'<tag title="tooltip">hello</tag>'}                                             | ${{ title: 'tooltip' }}
+    ${'time'} | ${'Lets meet at <time title="today" datetime="20:00">20:00</time>'}               | ${{ title: 'today', datetime: '20:00' }}
+    ${'abbr'} | ${'Sure, you can try it out but <abbr title="Your mileage may vary">YMMV</abbr>'} | ${{ title: 'Your mileage may vary' }}
+  `('extractAttributesFromMatch(inputRegex("$tag").exec(\'$input\'))', ({ tag, input, attrs }) => {
+    it(`returns: "${JSON.stringify(attrs)}"`, () => {
+      const matches = markInputRegex(tag).exec(input);
+      expect(extractMarkAttributesFromMatch(matches)).toEqual(attrs);
+    });
+  });
+});
diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml
index 6924423eecc6c7b016ab84fc63c6c743e3319c10..ff9da33c1168f49f1dee536f53f998cd27b26c1d 100644
--- a/spec/frontend/fixtures/api_markdown.yml
+++ b/spec/frontend/fixtures/api_markdown.yml
@@ -12,14 +12,27 @@
   markdown: |-
     * {-deleted-}
     * {+added+}
-- name: subscript
-  markdown: H<sub>2</sub>O
-- name: superscript
-  markdown: 2<sup>8</sup> = 256
 - name: strike
   markdown: '~~del~~'
 - name: horizontal_rule
   markdown: '---'
+- name: html_marks
+  markdown: |-
+    * Content editor is ~~great~~<ins>amazing</ins>.
+    * If the changes <abbr title="Looks good to merge">LGTM</abbr>, please <abbr title="Merge when pipeline succeeds">MWPS</abbr>.
+    * The English song <q>Oh I do like to be beside the seaside</q> looks like this in Hebrew: <span dir="rtl">אה, אני אוהב להיות ליד חוף הים</span>. In the computer's memory, this is stored as <bdo dir="ltr">אה, אני אוהב להיות ליד חוף הים</bdo>.
+    * <cite>The Scream</cite> by Edvard Munch. Painted in 1893.
+    * <dfn>HTML</dfn> is the standard markup language for creating web pages.
+    * Do not forget to buy <mark>milk</mark> today.
+    * This is a paragraph and <small>smaller text goes here</small>.
+    * The concert starts at <time datetime="20:00">20:00</time> and you'll be able to enjoy the band for at least <time datetime="PT2H30M">2h 30m</time>.
+    * Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text (Windows).
+    * WWF's goal is to: <q>Build a future where people live in harmony with nature.</q> We hope they succeed.
+    * The error occured was: <samp>Keyboard not found. Press F1 to continue.</samp>
+    * The area of a triangle is: 1/2 x <var>b</var> x <var>h</var>, where <var>b</var> is the base, and <var>h</var> is the vertical height.
+    * <ruby>漢<rt>ㄏㄢˋ</rt></ruby>
+    * C<sub>7</sub>H<sub>16</sub> + O<sub>2</sub> → CO<sub>2</sub> + H<sub>2</sub>O
+    * The **Pythagorean theorem** is often expressed as <var>a<sup>2</sup></var> + <var>b<sup>2</sup></var> = <var>c<sup>2</sup></var>
 - name: link
   markdown: '[GitLab](https://gitlab.com)'
 - name: attachment_link