mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-23 00:56:33 +03:00
chore: refactor components parsing in markdown
This commit is contained in:
parent
aeda21bcd5
commit
5924cd7ea2
|
@ -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) }} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
|
||||||
|
|
||||||
|
const regex = /([\w-]+)\s*=\s*(?:{([^}]+?)}|"([^"]+?)")/gim;
|
||||||
|
const parsed = {};
|
||||||
|
let match;
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
attrs[name] = value.startsWith('{') ? eval(value.substr(1, value.length - 2)) : eval(value);
|
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 */
|
||||||
}
|
}
|
||||||
return {
|
parsed[match[1]] = val;
|
||||||
componentName,
|
}
|
||||||
attrs,
|
}
|
||||||
};
|
|
||||||
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,42 @@ describe('Markdown renderer', () => {
|
||||||
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 ' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user