Commit 4a5a221e authored by Derek Knox's avatar Derek Knox Committed by Enrique Alcántara

Initial identifier custom renderer

Add initial custom renderer for the markdown
identifier syntax (reusable links and desc).
Minor refactor to render signatures too due
to need for node not just context.
parent 8e1be747
import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text'; import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierText from './renderers/render_identifier_text';
const listRenderers = [renderKramdownList]; const listRenderers = [renderKramdownList];
const textRenderers = [renderKramdownText]; const textRenderers = [renderKramdownText, renderIdentifierText];
const executeRenderer = (renderers, node, context) => { const executeRenderer = (renderers, node, context) => {
const availableRenderer = renderers.find(renderer => renderer.canRender(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) => { const buildCustomRendererFunctions = (customRenderers, defaults) => {
......
...@@ -16,6 +16,10 @@ export const buildUneditableOpenTokens = token => { ...@@ -16,6 +16,10 @@ export const buildUneditableOpenTokens = token => {
export const buildUneditableCloseToken = () => buildToken('closeTag', 'div'); export const buildUneditableCloseToken = () => buildToken('closeTag', 'div');
export const buildUneditableCloseTokens = token => {
return [token, buildToken('closeTag', 'div')];
};
export const buildUneditableTokens = token => { export const buildUneditableTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; 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 => { ...@@ -21,7 +21,7 @@ const canRender = node => {
return false; return false;
}; };
const render = ({ entering, origin }) => const render = (_, { entering, origin }) =>
entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken(); entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
export default { canRender, render }; export default { canRender, render };
import { buildUneditableTokens } from './build_uneditable_token'; import { buildUneditableTokens } from './build_uneditable_token';
const kramdownRegex = /(^{:.+}$)/;
const canRender = ({ literal }) => { const canRender = ({ literal }) => {
const kramdownRegex = /(^{:.+}$)/gm;
return kramdownRegex.test(literal); return kramdownRegex.test(literal);
}; };
const render = ({ origin }) => { const render = (_, { origin }) => {
return buildUneditableTokens(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 => { ...@@ -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 => { const buildMockListNode = literal => {
return { return {
firstChild: { firstChild: {
...@@ -23,6 +44,9 @@ export const kramdownListNode = buildMockListNode('TOC'); ...@@ -23,6 +44,9 @@ export const kramdownListNode = buildMockListNode('TOC');
export const normalListNode = buildMockListNode('Just another bullet point'); export const normalListNode = buildMockListNode('Just another bullet point');
export const kramdownTextNode = buildMockTextNode('{:toc}'); 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.'); export const normalTextNode = buildMockTextNode('This is just normal text.');
const uneditableOpenToken = { const uneditableOpenToken = {
...@@ -40,4 +64,5 @@ export const originToken = { ...@@ -40,4 +64,5 @@ export const originToken = {
content: '{:.no_toc .hidden-md .hidden-lg}', content: '{:.no_toc .hidden-md .hidden-lg}',
}; };
export const uneditableOpenTokens = [uneditableOpenToken, originToken]; export const uneditableOpenTokens = [uneditableOpenToken, originToken];
export const uneditableCloseTokens = [originToken, uneditableCloseToken];
export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken]; export const uneditableTokens = [...uneditableOpenTokens, uneditableCloseToken];
import { import {
buildUneditableOpenTokens, buildUneditableOpenTokens,
buildUneditableCloseToken, buildUneditableCloseToken,
buildUneditableCloseTokens,
buildUneditableTokens, buildUneditableTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token'; } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
...@@ -8,6 +9,7 @@ import { ...@@ -8,6 +9,7 @@ import {
originToken, originToken,
uneditableOpenTokens, uneditableOpenTokens,
uneditableCloseToken, uneditableCloseToken,
uneditableCloseTokens,
uneditableTokens, uneditableTokens,
} from '../../mock_data'; } from '../../mock_data';
...@@ -27,6 +29,15 @@ describe('Build Uneditable Token renderer helper', () => { ...@@ -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', () => { describe('buildUneditableTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle', () => { it('returns a 3-item array of tokens with the originToken wrapped in the middle', () => {
const result = buildUneditableTokens(originToken); 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', () => { ...@@ -23,13 +23,17 @@ describe('Render Kramdown List renderer', () => {
it('should return uneditable open tokens when entering', () => { it('should return uneditable open tokens when entering', () => {
const context = { entering: true, origin }; 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', () => { it('should return an uneditable close tokens when exiting', () => {
const context = { entering: false, origin }; 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', () => { ...@@ -20,7 +20,9 @@ describe('Render Kramdown Text renderer', () => {
it('should return uneditable tokens', () => { it('should return uneditable tokens', () => {
const context = { origin }; 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