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 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