Commit 26368f20 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '221082-custom-renderer-identifiers' into 'master'

Resolve "Display non-markdown content in the WYSIWYG mode of the SSE::Identifiers"

Closes #221082

See merge request gitlab-org/gitlab!35077
parents 536987c5 4a5a221e
import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierText from './renderers/render_identifier_text';
const listRenderers = [renderKramdownList];
const textRenderers = [renderKramdownText];
const textRenderers = [renderKramdownText, renderIdentifierText];
const executeRenderer = (renderers, node, context) => {
const availableRenderer = renderers.find(renderer => renderer.canRender(node, context));
return availableRenderer ? availableRenderer.render(context) : context.origin();
return availableRenderer ? availableRenderer.render(node, context) : context.origin();
};
const buildCustomRendererFunctions = (customRenderers, defaults) => {
......
......@@ -16,6 +16,10 @@ export const buildUneditableOpenTokens = token => {
export const buildUneditableCloseToken = () => buildToken('closeTag', 'div');
export const buildUneditableCloseTokens = token => {
return [token, buildToken('closeTag', 'div')];
};
export const buildUneditableTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
};
import {
buildUneditableOpenTokens,
buildUneditableCloseTokens,
buildUneditableTokens,
} from './build_uneditable_token';
const identifierRegex = /(^\[.+\]: .+)/;
const isBasicIdentifier = ({ literal }) => {
return identifierRegex.test(literal);
};
const isInlineCodeNode = ({ type, tickCount }) => type === 'code' && tickCount === 1;
const hasAdjacentInlineCode = (isForward, node) => {
const direction = isForward ? 'next' : 'prev';
let currentNode = node;
while (currentNode[direction] && currentNode.literal !== null) {
if (isInlineCodeNode(currentNode)) {
return true;
}
currentNode = currentNode[direction];
}
return false;
};
const hasEnteringPotential = literal => literal.includes('[');
const hasExitingPotential = literal => literal.includes(']: ');
const hasAdjacentExit = node => {
let currentNode = node;
while (currentNode.next && currentNode.literal !== null) {
if (hasExitingPotential(currentNode.literal)) {
return true;
}
currentNode = currentNode.next;
}
return false;
};
const isEnteringWithAdjacentInlineCode = ({ literal, next }) => {
if (next && hasEnteringPotential(literal) && !hasExitingPotential(literal)) {
return hasAdjacentInlineCode(true, next) && hasAdjacentExit(next);
}
return false;
};
const isExitingWithAdjacentInlineCode = ({ literal, prev }) => {
if (prev && !hasEnteringPotential(literal) && hasExitingPotential(literal)) {
return hasAdjacentInlineCode(false, prev);
}
return false;
};
const isAdjacentInlineCodeIdentifier = node => {
return isEnteringWithAdjacentInlineCode(node) || isExitingWithAdjacentInlineCode(node);
};
const canRender = (node, context) => {
return isBasicIdentifier(node) || isAdjacentInlineCodeIdentifier(node, context);
};
const render = (node, { origin }) => {
if (isEnteringWithAdjacentInlineCode(node)) {
return buildUneditableOpenTokens(origin());
} else if (isExitingWithAdjacentInlineCode(node)) {
return buildUneditableCloseTokens(origin());
}
return buildUneditableTokens(origin());
};
export default { canRender, render };
......@@ -21,7 +21,7 @@ const canRender = node => {
return false;
};
const render = ({ entering, origin }) =>
const render = (_, { entering, origin }) =>
entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
export default { canRender, render };
import { buildUneditableTokens } from './build_uneditable_token';
const kramdownRegex = /(^{:.+}$)/;
const canRender = ({ literal }) => {
const kramdownRegex = /(^{:.+}$)/gm;
return kramdownRegex.test(literal);
};
const render = ({ origin }) => {
const render = (_, { origin }) => {
return buildUneditableTokens(origin());
};
......
---
title: Add a custom HTML renderer to the Static Site Editor for markdown identifier syntax
merge_request: 35077
author:
type: added
......@@ -6,6 +6,27 @@ const buildMockTextNode = literal => {
};
};
const buildMockTextNodeWithAdjacentInlineCode = isForward => {
const direction = isForward ? 'next' : 'prev';
const literalOpen = '[';
const literalClose = ' file]: https://file.com/file.md';
return {
literal: isForward ? literalOpen : literalClose,
type: 'text',
[direction]: {
literal: 'raw',
tickCount: 1,
type: 'code',
[direction]: {
literal: isForward ? literalClose : literalOpen,
[direction]: {
literal: null,
},
},
},
};
};
const buildMockListNode = literal => {
return {
firstChild: {
......@@ -23,6 +44,9 @@ export const kramdownListNode = buildMockListNode('TOC');
export const normalListNode = buildMockListNode('Just another bullet point');
export const kramdownTextNode = buildMockTextNode('{:toc}');
export const identifierTextNode = buildMockTextNode('[Some text]: https://link.com');
export const identifierInlineCodeTextEnteringNode = buildMockTextNodeWithAdjacentInlineCode(true);
export const identifierInlineCodeTextExitingNode = buildMockTextNodeWithAdjacentInlineCode(false);
export const normalTextNode = buildMockTextNode('This is just normal text.');
const uneditableOpenToken = {
......@@ -40,4 +64,5 @@ export const originToken = {
content: '{:.no_toc .hidden-md .hidden-lg}',
};
export const uneditableOpenTokens = [uneditableOpenToken, originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
import {
buildUneditableOpenTokens,
buildUneditableCloseToken,
buildUneditableCloseTokens,
buildUneditableTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
......@@ -8,6 +9,7 @@ import {
originToken,
uneditableOpenTokens,
uneditableCloseToken,
uneditableCloseTokens,
uneditableTokens,
} from '../../mock_data';
......@@ -27,6 +29,15 @@ describe('Build Uneditable Token renderer helper', () => {
});
});
describe('buildUneditableCloseTokens', () => {
it('returns a 2-item array of tokens with the originToken prepended to a close token', () => {
const result = buildUneditableCloseTokens(originToken);
expect(result).toHaveLength(2);
expect(result).toStrictEqual(uneditableCloseTokens);
});
});
describe('buildUneditableTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle', () => {
const result = buildUneditableTokens(originToken);
......
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_text';
import {
buildUneditableOpenTokens,
buildUneditableCloseTokens,
buildUneditableTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers//build_uneditable_token';
import {
identifierTextNode,
identifierInlineCodeTextEnteringNode,
identifierInlineCodeTextExitingNode,
normalTextNode,
} from '../../mock_data';
describe('Render Identifier Text renderer', () => {
describe('canRender', () => {
it('should return true when the argument `literal` has identifier syntax', () => {
expect(renderer.canRender(identifierTextNode)).toBe(true);
});
it('should return true when the argument `literal` has identifier syntax and forward adjacent inline code', () => {
expect(renderer.canRender(identifierInlineCodeTextEnteringNode)).toBe(true);
});
it('should return true when the argument `literal` has identifier syntax and backward adjacent inline code', () => {
expect(renderer.canRender(identifierInlineCodeTextExitingNode)).toBe(true);
});
it('should return false when the argument `literal` lacks identifier syntax', () => {
expect(renderer.canRender(normalTextNode)).toBe(false);
});
});
describe('render', () => {
const origin = jest.fn();
it('should return uneditable tokens for basic identifier syntax', () => {
const context = { origin };
expect(renderer.render(identifierTextNode, context)).toStrictEqual(
buildUneditableTokens(origin()),
);
});
it('should return uneditable open tokens for non-basic inline code identifier syntax when entering', () => {
const context = { origin };
expect(renderer.render(identifierInlineCodeTextEnteringNode, context)).toStrictEqual(
buildUneditableOpenTokens(origin()),
);
});
it('should return uneditable close tokens for non-basic inline code identifier syntax when exiting', () => {
const context = { origin };
expect(renderer.render(identifierInlineCodeTextExitingNode, context)).toStrictEqual(
buildUneditableCloseTokens(origin()),
);
});
});
});
......@@ -23,13 +23,17 @@ describe('Render Kramdown List renderer', () => {
it('should return uneditable open tokens when entering', () => {
const context = { entering: true, origin };
expect(renderer.render(context)).toStrictEqual(buildUneditableOpenTokens(origin()));
expect(renderer.render(kramdownListNode, context)).toStrictEqual(
buildUneditableOpenTokens(origin()),
);
});
it('should return an uneditable close tokens when exiting', () => {
const context = { entering: false, origin };
expect(renderer.render(context)).toStrictEqual(buildUneditableCloseToken(origin()));
expect(renderer.render(kramdownListNode, context)).toStrictEqual(
buildUneditableCloseToken(origin()),
);
});
});
});
......@@ -20,7 +20,9 @@ describe('Render Kramdown Text renderer', () => {
it('should return uneditable tokens', () => {
const context = { origin };
expect(renderer.render(context)).toStrictEqual(buildUneditableTokens(origin()));
expect(renderer.render(kramdownTextNode, context)).toStrictEqual(
buildUneditableTokens(origin()),
);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment