Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/rich_content_editor/services')
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js68
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js53
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js56
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js63
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js13
11 files changed, 369 insertions, 0 deletions
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
new file mode 100644
index 00000000000..70d29b5b3df
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -0,0 +1,68 @@
+import renderBlockHtml from './renderers/render_html_block';
+import renderKramdownList from './renderers/render_kramdown_list';
+import renderKramdownText from './renderers/render_kramdown_text';
+import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
+import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
+import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
+import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
+
+const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
+const htmlBlockRenderers = [renderBlockHtml];
+const listRenderers = [renderKramdownList];
+const paragraphRenderers = [renderIdentifierParagraph];
+const textRenderers = [renderKramdownText, renderEmbeddedRubyText, renderIdentifierInstanceText];
+
+const executeRenderer = (renderers, node, context) => {
+ const availableRenderer = renderers.find(renderer => renderer.canRender(node, context));
+
+ return availableRenderer ? availableRenderer.render(node, context) : context.origin();
+};
+
+const buildCustomRendererFunctions = (customRenderers, defaults) => {
+ const customTypes = Object.keys(customRenderers).filter(type => !defaults[type]);
+ const customEntries = customTypes.map(type => {
+ const fn = (node, context) => executeRenderer(customRenderers[type], node, context);
+ return [type, fn];
+ });
+
+ return Object.fromEntries(customEntries);
+};
+
+const buildCustomHTMLRenderer = (
+ customRenderers = { htmlBlock: [], htmlInline: [], list: [], paragraph: [], text: [] },
+) => {
+ const defaults = {
+ htmlBlock(node, context) {
+ const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers];
+
+ return executeRenderer(allHtmlBlockRenderers, node, context);
+ },
+ htmlInline(node, context) {
+ const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
+
+ return executeRenderer(allHtmlInlineRenderers, node, context);
+ },
+ list(node, context) {
+ const allListRenderers = [...customRenderers.list, ...listRenderers];
+
+ return executeRenderer(allListRenderers, node, context);
+ },
+ paragraph(node, context) {
+ const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
+
+ return executeRenderer(allParagraphRenderers, node, context);
+ },
+ text(node, context) {
+ const allTextRenderers = [...customRenderers.text, ...textRenderers];
+
+ return executeRenderer(allTextRenderers, node, context);
+ },
+ };
+
+ return {
+ ...buildCustomRendererFunctions(customRenderers, defaults),
+ ...defaults,
+ };
+};
+
+export default buildCustomHTMLRenderer;
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
new file mode 100644
index 00000000000..ed04765c871
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
@@ -0,0 +1,53 @@
+import { defaults, repeat } from 'lodash';
+
+const DEFAULTS = {
+ subListIndentSpaces: 4,
+};
+
+const countIndentSpaces = text => {
+ const matches = text.match(/^\s+/m);
+
+ return matches ? matches[0].length : 0;
+};
+
+const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
+ const { subListIndentSpaces } = defaults(formattingPreferences, DEFAULTS);
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const sublistNode = 'LI OL, LI UL';
+
+ return {
+ TEXT_NODE(node) {
+ return baseRenderer.getSpaceControlled(
+ baseRenderer.trim(baseRenderer.getSpaceCollapsedText(node.nodeValue)),
+ node,
+ );
+ },
+ /*
+ * This converter overwrites the default indented list converter
+ * to allow us to parameterize the number of indent spaces for
+ * sublists.
+ *
+ * See the original implementation in
+ * https://github.com/nhn/tui.editor/blob/master/libs/to-mark/src/renderer.basic.js#L161
+ */
+ [sublistNode](node, subContent) {
+ const baseResult = baseRenderer.convert(node, subContent);
+ // Default to 1 to prevent possible divide by 0
+ const firstLevelIndentSpacesCount = countIndentSpaces(baseResult) || 1;
+ const reindentedList = baseResult
+ .split('\n')
+ .map(line => {
+ const itemIndentSpacesCount = countIndentSpaces(line);
+ const nestingLevel = Math.ceil(itemIndentSpacesCount / firstLevelIndentSpacesCount);
+ const indentSpaces = repeat(' ', subListIndentSpaces * nestingLevel);
+
+ return line.replace(/^ +/, indentSpaces);
+ })
+ .join('\n');
+
+ return reindentedList;
+ },
+ };
+};
+
+export default buildHTMLToMarkdownRender;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
new file mode 100644
index 00000000000..6436dcaae64
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import ToolbarItem from '../toolbar_item.vue';
+import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
+
+const buildWrapper = propsData => {
+ const instance = new Vue({
+ render(createElement) {
+ return createElement(ToolbarItem, propsData);
+ },
+ });
+
+ instance.$mount();
+ return instance.$el;
+};
+
+export const generateToolbarItem = config => {
+ const { icon, classes, event, command, tooltip, isDivider } = config;
+
+ if (isDivider) {
+ return 'divider';
+ }
+
+ return {
+ type: 'button',
+ options: {
+ el: buildWrapper({ props: { icon, tooltip }, class: classes }),
+ event,
+ command,
+ },
+ };
+};
+
+export const addCustomEventListener = (editorApi, event, handler) => {
+ editorApi.eventManager.addEventType(event);
+ editorApi.eventManager.listen(event, handler);
+};
+
+export const removeCustomEventListener = (editorApi, event, handler) =>
+ editorApi.eventManager.removeEventHandler(event, handler);
+
+export const addImage = ({ editor }, image) => editor.exec('AddImage', image);
+
+export const getMarkdown = editorInstance => editorInstance.invoke('getMarkdown');
+
+/**
+ * This function allow us to extend Toast UI HTML to Markdown renderer. It is
+ * a temporary measure because Toast UI does not provide an API
+ * to achieve this goal.
+ */
+export const registerHTMLToMarkdownRenderer = editorApi => {
+ const { renderer } = editorApi.toMarkOptions;
+
+ Object.assign(editorApi.toMarkOptions, {
+ renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)),
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
new file mode 100644
index 00000000000..d96cadafdbb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
@@ -0,0 +1,63 @@
+const buildToken = (type, tagName, props) => {
+ return { type, tagName, ...props };
+};
+
+const TAG_TYPES = {
+ block: 'div',
+ inline: 'a',
+};
+
+// 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-my-5 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)];
+};
+
+// Complete helpers (open plus close)
+
+export const buildTextToken = content => buildToken('text', null, { content });
+
+export const buildUneditableTokens = token => {
+ return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
+};
+
+export const buildUneditableInlineTokens = token => {
+ return [
+ ...buildUneditableOpenTokens(token, TAG_TYPES.inline),
+ buildUneditableCloseToken(TAG_TYPES.inline),
+ ];
+};
+
+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()];
+};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
new file mode 100644
index 00000000000..494057fc75b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
@@ -0,0 +1,13 @@
+import { buildUneditableTokens } from './build_uneditable_token';
+
+const embeddedRubyRegex = /(^<%.+%>$)/;
+
+const canRender = ({ literal }) => {
+ return embeddedRubyRegex.test(literal);
+};
+
+const render = (_, { origin }) => {
+ return buildUneditableTokens(origin());
+};
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
new file mode 100644
index 00000000000..572f6e3cf9d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
@@ -0,0 +1,11 @@
+import { buildUneditableInlineTokens } from './build_uneditable_token';
+
+const fontAwesomeRegexOpen = /<i class="fa.+>/;
+
+const canRender = ({ literal }) => {
+ return fontAwesomeRegexOpen.test(literal);
+};
+
+const render = (_, { origin }) => buildUneditableInlineTokens(origin());
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
new file mode 100644
index 00000000000..b179ca61dba
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js
@@ -0,0 +1,9 @@
+import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
+
+const canRender = ({ type }) => {
+ return type === 'htmlBlock';
+};
+
+const render = node => buildUneditableHtmlAsTextTokens(node);
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js
new file mode 100644
index 00000000000..a9c3dfcd728
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js
@@ -0,0 +1,40 @@
+import { buildTextToken, buildUneditableInlineTokens } from './build_uneditable_token';
+
+/*
+Use case examples:
+- Majority: two bracket pairs, back-to-back, each with content (including spaces)
+ - `[environment terraform plans][terraform]`
+ - `[an issue labelled `~"master:broken"`][broken-master-issues]`
+- Minority: two bracket pairs the latter being empty or only one pair with content (including spaces)
+ - `[this link][]`
+ - `[this link]`
+
+Regexp notes:
+ - `(?:\[.+?\]){1}`: Always one bracket pair with content (including spaces)
+ - `(?:\[\]|\[.+?\])?`: Optional second pair that may or may not contain content (including spaces)
+ - `(?!:)`: Never followed by a `:` which is reserved for identifier definition syntax (`[identifier]: /the-link`)
+ - Each of the three parts is non-captured, but the match as a whole is captured
+*/
+const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g;
+
+const isIdentifierInstance = literal => {
+ // Reset lastIndex as global flag in regexp are stateful (https://stackoverflow.com/a/11477448)
+ identifierInstanceRegex.lastIndex = 0;
+ return identifierInstanceRegex.test(literal);
+};
+
+const canRender = ({ literal }) => isIdentifierInstance(literal);
+
+const tokenize = text => {
+ const matches = text.split(identifierInstanceRegex);
+ const tokens = matches.map(match => {
+ const token = buildTextToken(match);
+ return isIdentifierInstance(match) ? buildUneditableInlineTokens(token) : token;
+ });
+
+ return tokens.flat();
+};
+
+const render = (_, { origin }) => tokenize(origin().content);
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
new file mode 100644
index 00000000000..f5b4502ea3c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
@@ -0,0 +1,16 @@
+import { buildUneditableOpenTokens, buildUneditableCloseToken } from './build_uneditable_token';
+
+const identifierRegex = /(^\[.+\]: .+)/;
+
+const isIdentifier = text => {
+ return identifierRegex.test(text);
+};
+
+const canRender = (node, context) => {
+ return isIdentifier(context.getChildrenText(node));
+};
+
+const render = (_, { entering, origin }) =>
+ entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
+
+export default { canRender, render };
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
new file mode 100644
index 00000000000..491a26c81d0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
@@ -0,0 +1,27 @@
+import { buildUneditableOpenTokens, buildUneditableCloseToken } from './build_uneditable_token';
+
+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;
+};
+
+const render = (_, { entering, origin }) =>
+ entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
+
+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
new file mode 100644
index 00000000000..01384699e4f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
@@ -0,0 +1,13 @@
+import { buildUneditableTokens } from './build_uneditable_token';
+
+const kramdownRegex = /(^{:.+}$)/;
+
+const canRender = ({ literal }) => {
+ return kramdownRegex.test(literal);
+};
+
+const render = (_, { origin }) => {
+ return buildUneditableTokens(origin());
+};
+
+export default { canRender, render };