Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Map, OrderedMap } from 'immutable';
import { Map } from 'immutable';

import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes';
import { remarkParseShortcodes } from '../remarkShortcodes';

// Stub of Remark Parser
function process(value, plugins, processEat = () => {}) {
Expand All @@ -26,14 +26,9 @@ function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) {
describe('remarkParseShortcodes', () => {
describe('pattern matching', () => {
it('should work', () => {
const editorComponent = EditorComponent({ pattern: /bar/ });
const editorComponent = EditorComponent({ pattern: /^foo/ });
process('foo bar', Map({ [editorComponent.id]: editorComponent }));
expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
});
it('should match value surrounded in newlines', () => {
const editorComponent = EditorComponent({ pattern: /^bar$/ });
process('foo\n\nbar\n', Map({ [editorComponent.id]: editorComponent }));
expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo']));
});
it('should match multiline shortcodes', () => {
const editorComponent = EditorComponent({ pattern: /^foo\nbar$/ });
Expand All @@ -47,60 +42,15 @@ describe('remarkParseShortcodes', () => {
expect.arrayContaining(['foo\n\nbar']),
);
});
it('should match shortcodes based on order of occurrence in value', () => {
const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ });
const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
process(
'foo\n\nbar',
OrderedMap([
[barEditorComponent.id, barEditorComponent],
[fooEditorComponent.id, fooEditorComponent],
]),
);
expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo']));
});
it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => {
const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ });
process(
'foo\n\nbar\n\nbaz',
OrderedMap([
[bazEditorComponent.id, bazEditorComponent],
[barEditorComponent.id, barEditorComponent],
]),
);
expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
});
});
describe('output', () => {
it('should be a remark shortcode node', () => {
const processEat = jest.fn();
const shortcodeData = { bar: 'baz' };
const expectedNode = { type: 'shortcode', data: { shortcode: 'foo', shortcodeData } };
const editorComponent = EditorComponent({ pattern: /bar/, fromBlock: () => shortcodeData });
const editorComponent = EditorComponent({ pattern: /^foo/, fromBlock: () => shortcodeData });
process('foo bar', Map({ [editorComponent.id]: editorComponent }), processEat);
expect(processEat).toHaveBeenCalledWith(expectedNode);
});
});
});

describe('getLinesWithOffsets', () => {
test('should split into lines', () => {
const value = ' line1\n\nline2 \n\n line3 \n\n';

const lines = getLinesWithOffsets(value);
expect(lines).toEqual([
{ line: ' line1', start: 0 },
{ line: 'line2', start: 8 },
{ line: ' line3', start: 16 },
{ line: '', start: 30 },
]);
});

test('should return single item on no match', () => {
const value = ' line1 ';

const lines = getLinesWithOffsets(value);
expect(lines).toEqual([{ line: ' line1', start: 0 }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,65 +8,32 @@ export function remarkParseShortcodes({ plugins }) {
methods.unshift('shortcode');
}

export function getLinesWithOffsets(value) {
const SEPARATOR = '\n\n';
const splitted = value.split(SEPARATOR);
const trimmedLines = splitted
.reduce(
(acc, line) => {
const { start: previousLineStart, originalLength: previousLineOriginalLength } =
acc[acc.length - 1];

return [
...acc,
{
line: line.trimEnd(),
start: previousLineStart + previousLineOriginalLength + SEPARATOR.length,
originalLength: line.length,
},
];
},
[{ start: -SEPARATOR.length, originalLength: 0 }],
)
.slice(1)
.map(({ line, start }) => ({ line, start }));
return trimmedLines;
}

function matchFromLines({ trimmedLines, plugin }) {
for (const { line, start } of trimmedLines) {
const match = line.match(plugin.pattern);
if (match) {
match.index += start;
return match;
}
}
}

function createShortcodeTokenizer({ plugins }) {
plugins.forEach(plugin => {
if (plugin.pattern.flags.includes('m')) {
console.warn(
`Invalid RegExp: editor component '${plugin.id}' must not use the multiline flag in its pattern.`,
);
}
});
return function tokenizeShortcode(eat, value, silent) {
// Plugin patterns may rely on `^` and `$` tokens, even if they don't
// use the multiline flag. To support this, we fall back to searching
// through each line individually, trimming trailing whitespace and
// newlines, if we don't initially match on a pattern. We keep track of
// the starting position of each line so that we can sort correctly
// across the full multiline matches.
const trimmedLines = getLinesWithOffsets(value);
let match;
const potentialMatchValue = value.split('\n\n')[0].trimEnd();
const plugin = plugins.find(plugin => {
match = value.match(plugin.pattern);
if (!match) {
match = potentialMatchValue.match(plugin.pattern);
}

// Attempt to find a regex match for each plugin's pattern, and then
// select the first by its occurrence in `value`. This ensures we won't
// skip a plugin that occurs later in the plugin registry, but earlier
// in the `value`.
const [{ plugin, match } = {}] = plugins
.toArray()
.map(plugin => ({
match: value.match(plugin.pattern) || matchFromLines({ trimmedLines, plugin }),
plugin,
}))
.filter(({ match }) => !!match)
.sort((a, b) => a.match.index - b.match.index);
return !!match;
});

if (match) {
if (match.index > 0) {
console.warn(
`Invalid RegExp: editor component '${plugin.id}' must match from the beginning of the block.`,
);
}
if (silent) {
return true;
}
Expand Down