chore: refactor components parsing in markdown

This commit is contained in:
Roman Hotsiy 2018-10-04 10:49:43 +03:00
parent aeda21bcd5
commit 5924cd7ea2
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
3 changed files with 98 additions and 61 deletions

View File

@ -42,7 +42,7 @@ export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
{ key: idx }, { key: idx },
); );
} }
return <part.component key={idx} {...{ ...part.attrs, ...part.propsSelector(store) }} />; return <part.component key={idx} {...{ ...part.props, ...part.propsSelector(store) }} />;
}); });
} }
} }

View File

@ -13,14 +13,18 @@ marked.setOptions({
}, },
}); });
export const LEGACY_REGEXP = '^\\s*<!-- ReDoc-Inject:\\s+?<{component}\\s*?/?>\\s+?-->\\s*$'; export const LEGACY_REGEXP = '^ {0,3}<!-- ReDoc-Inject:\\s+?<({component}).*?/?>\\s+?-->\\s*$';
export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$';
// prettier-ignore
export const MDX_COMPONENT_REGEXP = '(?:^ {0,3}<({component})([\\s\\S]*?)>([\\s\\S]*?)</\\2>' // with children
+ '|^ {0,3}<({component})([\\s\\S]*?)(?:/>|\\n{2,}))'; // self-closing
export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')'; export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')';
export interface MDXComponentMeta { export interface MDXComponentMeta {
component: React.ComponentType; component: React.ComponentType;
propsSelector: (store?: AppStore) => any; propsSelector: (store?: AppStore) => any;
attrs?: object; props?: object;
} }
export interface MarkdownHeading { export interface MarkdownHeading {
@ -37,11 +41,8 @@ export function buildComponentComment(name: string) {
export class MarkdownRenderer { export class MarkdownRenderer {
static containsComponent(rawText: string, componentName: string) { static containsComponent(rawText: string, componentName: string) {
const anyCompRegexp = new RegExp( const compRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, componentName), 'gmi');
COMPONENT_REGEXP.replace(/{component}/g, componentName), return compRegexp.test(rawText);
'gmi',
);
return anyCompRegexp.test(rawText);
} }
headings: MarkdownHeading[] = []; headings: MarkdownHeading[] = [];
@ -147,32 +148,41 @@ export class MarkdownRenderer {
return res; return res;
} }
// TODO: rewrite this completelly! Regexp-based 👎 // regexp-based 👎: remark is slow and too big so for now using marked + regexps soup
// Use marked ecosystem
renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> { renderMdWithComponents(rawText: string): Array<string | MDXComponentMeta> {
const components = this.options && this.options.allowedMdComponents; const components = this.options && this.options.allowedMdComponents;
if (!components || Object.keys(components).length === 0) { if (!components || Object.keys(components).length === 0) {
return [this.renderMd(rawText)]; return [this.renderMd(rawText)];
} }
const componentDefs: string[] = []; const names = Object.keys(components).join('|');
const names = '(?:' + Object.keys(components).join('|') + ')'; const componentsRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, names), 'mig');
const anyCompRegexp = new RegExp( const htmlParts: string[] = [];
COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'), const componentDefs: MDXComponentMeta[] = [];
'gmi',
); let match = componentsRegexp.exec(rawText);
let match = anyCompRegexp.exec(rawText); let lasxtIdx = 0;
while (match) { while (match) {
componentDefs.push(match[1] || match[2]); htmlParts.push(rawText.substring(lasxtIdx, match.index));
match = anyCompRegexp.exec(rawText); lasxtIdx = componentsRegexp.lastIndex;
} const compName = match[1] || match[2] || match[5];
const componentMeta = components[compName];
const props = match[3] || match[6];
const children = match[4];
if (componentMeta) {
componentDefs.push({
component: componentMeta.component,
propsSelector: componentMeta.propsSelector,
props: { ...parseProps(props), ...componentMeta.props, children },
});
}
match = componentsRegexp.exec(rawText);
}
htmlParts.push(rawText.substring(lasxtIdx));
const splitCompRegexp = new RegExp(
COMPONENT_REGEXP.replace(/{component}/g, names + '.*?'),
'mi',
);
const htmlParts = rawText.split(splitCompRegexp);
const res: any[] = []; const res: any[] = [];
for (let i = 0; i < htmlParts.length; i++) { for (let i = 0; i < htmlParts.length; i++) {
const htmlPart = htmlParts[i]; const htmlPart = htmlParts[i];
@ -180,46 +190,37 @@ export class MarkdownRenderer {
res.push(this.renderMd(htmlPart)); res.push(this.renderMd(htmlPart));
} }
if (componentDefs[i]) { if (componentDefs[i]) {
const { componentName, attrs } = parseComponent(componentDefs[i]); res.push(componentDefs[i]);
if (!componentName) {
continue;
}
res.push({
...components[componentName],
attrs,
});
} }
} }
return res; return res;
} }
} }
function parseComponent( function parseProps(props: string): object {
htmlTag: string, if (!props) {
): { return {};
componentName?: string;
attrs: any;
} {
const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag);
if (match === null || match.length <= 1) {
return { componentName: undefined, attrs: {} };
} }
const componentName = match[1];
const attrs = {};
for (let i = 2; i < match.length; i++) {
if (!match[i]) {
continue;
}
const [name, value] = match[i]
.trim()
.split('=')
.map(p => p.trim());
// tslint:disable-next-line const regex = /([\w-]+)\s*=\s*(?:{([^}]+?)}|"([^"]+?)")/gim;
attrs[name] = value.startsWith('{') ? eval(value.substr(1, value.length - 2)) : eval(value); const parsed = {};
let match;
// tslint:disable-next-line
while ((match = regex.exec(props)) !== null) {
if (match[3]) {
// string prop match (in double quotes)
parsed[match[1]] = match[3];
} else if (match[2]) {
// jsx prop match (in curly braces)
let val;
try {
val = JSON.parse(match[2]);
} catch (e) {
/* noop */
}
parsed[match[1]] = val;
}
} }
return {
componentName, return parsed;
attrs,
};
} }

View File

@ -53,11 +53,47 @@ describe('Markdown renderer', () => {
}); });
test('renderMdWithComponents should parse attribute names', () => { test('renderMdWithComponents should parse attribute names', () => {
const source = '<security-definitions pointer={"test"}/>'; const source = '<security-definitions pointer={"test"} />';
const parts = renderer.renderMdWithComponents(source); const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1); expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta; const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent); expect(part.component).toBe(TestComponent);
expect(part.attrs).toEqual({ pointer: 'test' }); expect(part.props).toEqual({ pointer: 'test' });
});
test('renderMdWithComponents should parse string attribute names', () => {
const source = '<security-definitions pointer="test" />';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ pointer: 'test' });
});
test('renderMdWithComponents should parse string attribute with spaces new-lines', () => {
const source = '<security-definitions \n pointer = "test" \n flag-dash={ \nfalse } />';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ pointer: 'test', 'flag-dash': false });
});
test('renderMdWithComponents should parse children', () => {
const source = '<security-definitions> Test Test </security-definitions>';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ children: ' Test Test ' });
});
test('renderMdWithComponents should parse children', () => {
const source = '<security-definitions> Test Test </security-definitions>';
const parts = renderer.renderMdWithComponents(source);
expect(parts).toHaveLength(1);
const part = parts[0] as MDXComponentMeta;
expect(part.component).toBe(TestComponent);
expect(part.props).toEqual({ children: ' Test Test ' });
}); });
}); });