From 5924cd7ea29a81145c9d91cf70a32c874da37387 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Thu, 4 Oct 2018 10:49:43 +0300 Subject: [PATCH] chore: refactor components parsing in markdown --- src/components/Markdown/AdvancedMarkdown.tsx | 2 +- src/services/MarkdownRenderer.ts | 117 +++++++++--------- .../__tests__/MarkdownRenderer.test.ts | 40 +++++- 3 files changed, 98 insertions(+), 61 deletions(-) diff --git a/src/components/Markdown/AdvancedMarkdown.tsx b/src/components/Markdown/AdvancedMarkdown.tsx index 0b1bbed7..d7714f5e 100644 --- a/src/components/Markdown/AdvancedMarkdown.tsx +++ b/src/components/Markdown/AdvancedMarkdown.tsx @@ -42,7 +42,7 @@ export class AdvancedMarkdown extends React.Component { { key: idx }, ); } - return ; + return ; }); } } diff --git a/src/services/MarkdownRenderer.ts b/src/services/MarkdownRenderer.ts index 3494415b..a944dac3 100644 --- a/src/services/MarkdownRenderer.ts +++ b/src/services/MarkdownRenderer.ts @@ -13,14 +13,18 @@ marked.setOptions({ }, }); -export const LEGACY_REGEXP = '^\\s*\\s*$'; -export const MDX_COMPONENT_REGEXP = '^\\s*<{component}\\s*?/>\\s*$'; +export const LEGACY_REGEXP = '^ {0,3}\\s*$'; + +// prettier-ignore +export const MDX_COMPONENT_REGEXP = '(?:^ {0,3}<({component})([\\s\\S]*?)>([\\s\\S]*?)' // with children + + '|^ {0,3}<({component})([\\s\\S]*?)(?:/>|\\n{2,}))'; // self-closing + export const COMPONENT_REGEXP = '(?:' + LEGACY_REGEXP + '|' + MDX_COMPONENT_REGEXP + ')'; export interface MDXComponentMeta { component: React.ComponentType; propsSelector: (store?: AppStore) => any; - attrs?: object; + props?: object; } export interface MarkdownHeading { @@ -37,11 +41,8 @@ export function buildComponentComment(name: string) { export class MarkdownRenderer { static containsComponent(rawText: string, componentName: string) { - const anyCompRegexp = new RegExp( - COMPONENT_REGEXP.replace(/{component}/g, componentName), - 'gmi', - ); - return anyCompRegexp.test(rawText); + const compRegexp = new RegExp(COMPONENT_REGEXP.replace(/{component}/g, componentName), 'gmi'); + return compRegexp.test(rawText); } headings: MarkdownHeading[] = []; @@ -147,32 +148,41 @@ export class MarkdownRenderer { return res; } - // TODO: rewrite this completelly! Regexp-based 👎 - // Use marked ecosystem + // regexp-based 👎: remark is slow and too big so for now using marked + regexps soup renderMdWithComponents(rawText: string): Array { const components = this.options && this.options.allowedMdComponents; if (!components || Object.keys(components).length === 0) { 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( - COMPONENT_REGEXP.replace(/{component}/g, '(' + names + '.*?)'), - 'gmi', - ); - let match = anyCompRegexp.exec(rawText); + const htmlParts: string[] = []; + const componentDefs: MDXComponentMeta[] = []; + + let match = componentsRegexp.exec(rawText); + let lasxtIdx = 0; while (match) { - componentDefs.push(match[1] || match[2]); - match = anyCompRegexp.exec(rawText); - } + htmlParts.push(rawText.substring(lasxtIdx, match.index)); + 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[] = []; for (let i = 0; i < htmlParts.length; i++) { const htmlPart = htmlParts[i]; @@ -180,46 +190,37 @@ export class MarkdownRenderer { res.push(this.renderMd(htmlPart)); } if (componentDefs[i]) { - const { componentName, attrs } = parseComponent(componentDefs[i]); - if (!componentName) { - continue; - } - res.push({ - ...components[componentName], - attrs, - }); + res.push(componentDefs[i]); } } return res; } } -function parseComponent( - htmlTag: string, -): { - componentName?: string; - attrs: any; -} { - const match = /([\w_-]+)(\s+[\w_-]+\s*={[^}]*?})*/.exec(htmlTag); - if (match === null || match.length <= 1) { - return { componentName: undefined, attrs: {} }; +function parseProps(props: string): object { + if (!props) { + return {}; } - 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 - attrs[name] = value.startsWith('{') ? eval(value.substr(1, value.length - 2)) : eval(value); + const regex = /([\w-]+)\s*=\s*(?:{([^}]+?)}|"([^"]+?)")/gim; + 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, - attrs, - }; + + return parsed; } diff --git a/src/services/__tests__/MarkdownRenderer.test.ts b/src/services/__tests__/MarkdownRenderer.test.ts index 89f308c6..efd0d1fb 100644 --- a/src/services/__tests__/MarkdownRenderer.test.ts +++ b/src/services/__tests__/MarkdownRenderer.test.ts @@ -53,11 +53,47 @@ describe('Markdown renderer', () => { }); test('renderMdWithComponents should parse attribute names', () => { - const source = ''; + const source = ''; const parts = renderer.renderMdWithComponents(source); expect(parts).toHaveLength(1); const part = parts[0] as MDXComponentMeta; 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 = ''; + 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 = ''; + 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 = ' Test 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({ children: ' Test Test ' }); + }); + + test('renderMdWithComponents should parse children', () => { + const source = ' Test 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({ children: ' Test Test ' }); }); });