summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers')
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js63
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js7
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js9
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js11
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_heading.js6
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js23
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js40
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js40
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js6
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js7
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js38
11 files changed, 250 insertions, 0 deletions
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token.js
new file mode 100644
index 00000000000..638e5fd6f60
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/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 buildUneditableBlockTokens = (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/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition.js
new file mode 100644
index 00000000000..bd419447a48
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/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/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js
new file mode 100644
index 00000000000..0e122f598e5
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text.js
@@ -0,0 +1,9 @@
+import { renderUneditableLeaf as render } from './render_utils';
+
+const embeddedRubyRegex = /(^<%.+%>$)/;
+
+const canRender = ({ literal }) => {
+ return embeddedRubyRegex.test(literal);
+};
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline.js
new file mode 100644
index 00000000000..572f6e3cf9d
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/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/static_site_editor/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_heading.js
new file mode 100644
index 00000000000..71026fd0d65
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/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/static_site_editor/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js
new file mode 100644
index 00000000000..710b807275b
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_html_block.js
@@ -0,0 +1,23 @@
+import { getURLOrigin } from '~/lib/utils/url_utility';
+import { ALLOWED_VIDEO_ORIGINS } from '../../constants';
+import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token';
+
+const isVideoFrame = (html) => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const {
+ children: { length },
+ } = doc;
+ const iframe = doc.querySelector('iframe');
+ const origin = iframe && getURLOrigin(iframe.getAttribute('src'));
+
+ return length === 1 && ALLOWED_VIDEO_ORIGINS.includes(origin);
+};
+
+const canRender = ({ type, literal }) => {
+ return type === 'htmlBlock' && !isVideoFrame(literal);
+};
+
+const render = (node) => buildUneditableHtmlAsTextTokens(node);
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js
new file mode 100644
index 00000000000..d770dd18d7f
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/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 `~"main:broken"`][broken-main-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/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js
new file mode 100644
index 00000000000..4829f0f2243
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph.js
@@ -0,0 +1,40 @@
+const identifierRegex = /(^\[.+\]: .+)/;
+
+const isIdentifier = (text) => {
+ return identifierRegex.test(text);
+};
+
+const canRender = (node, context) => {
+ return isIdentifier(context.getChildrenText(node));
+};
+
+const getReferenceDefinitions = (node, definitions = '') => {
+ if (!node) {
+ return definitions;
+ }
+
+ const definition = node.type === 'text' ? node.literal : '\n';
+
+ return getReferenceDefinitions(node.next, `${definitions}${definition}`);
+};
+
+const render = (node, { skipChildren }) => {
+ const content = getReferenceDefinitions(node.firstChild);
+
+ skipChildren();
+
+ return [
+ {
+ type: 'openTag',
+ tagName: 'pre',
+ classNames: ['code-block', 'language-markdown'],
+ attributes: { 'data-sse-reference-definition': true },
+ },
+ { type: 'openTag', tagName: 'code' },
+ { type: 'text', content },
+ { type: 'closeTag', tagName: 'code' },
+ { type: 'closeTag', tagName: 'pre' },
+ ];
+};
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_list_item.js
new file mode 100644
index 00000000000..71026fd0d65
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/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/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js
new file mode 100644
index 00000000000..c004e839821
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_softbreak.js
@@ -0,0 +1,7 @@
+const canRender = (node) => ['emph', 'strong'].includes(node.parent?.type);
+const render = () => ({
+ type: 'text',
+ content: ' ',
+});
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js
new file mode 100644
index 00000000000..eff5dbf59f2
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_utils.js
@@ -0,0 +1,38 @@
+import {
+ buildUneditableBlockTokens,
+ buildUneditableOpenTokens,
+ buildUneditableCloseToken,
+} from './build_uneditable_token';
+
+export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockTokens(origin());
+
+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;