@readme/markdown
Version:
ReadMe's React-based Markdown parser
222 lines (207 loc) • 5.79 kB
JavaScript
/* eslint-disable consistent-return */
const RGXP = /^\[block:(.*)\]([^]+?)\[\/block\]/;
const WrapPinnedBlocks = (node, json) => {
if (!json.sidebar) return node;
return {
children: [node],
type: 'rdme-pin',
data: {
hName: 'rdme-pin',
className: 'pin',
},
};
};
function tokenize(eat, value) {
let [match, type, json] = RGXP.exec(value) || [];
if (!type) return;
match = match.trim();
type = type.trim();
try {
json = JSON.parse(json);
} catch (err) {
json = {};
// eslint-disable-next-line no-console
console.error('Invalid Magic Block JSON:', err);
}
if (Object.keys(json).length < 1) return eat(match);
switch (type) {
case 'code': {
const children = json.codes.map(obj => ({
type: 'code',
value: obj.code,
meta: obj.name || null,
lang: obj.language,
className: 'tab-panel',
data: {
hName: 'code',
hProperties: {
meta: obj.name || null,
lang: obj.language,
},
},
}));
if (children.length === 1) return eat(match)(WrapPinnedBlocks(children[0], json));
return eat(match)(
WrapPinnedBlocks(
{
children,
className: 'tabs',
data: { hName: 'code-tabs' },
type: 'code-tabs',
},
json
)
);
}
case 'api-header': {
return eat(match)(
WrapPinnedBlocks(
{
type: 'heading',
depth: json.level || 2,
children: 'title' in json ? this.tokenizeInline(json.title, eat.now()) : '',
},
json
)
);
}
case 'image': {
const imgs = json.images.map(img => {
const [url, title] = img.image;
return {
type: 'image',
url,
title,
alt: img.caption,
data: {
hProperties: {
caption: img.caption,
},
},
};
});
const img = imgs[0];
if (!img.url) return eat(match);
return eat(match)(WrapPinnedBlocks(img, json));
}
case 'callout': {
const types = {
info: ['ℹ', 'info'],
success: ['👍', 'okay'],
warning: ['⚠️', 'warn'],
danger: ['❗️', 'error'],
};
json.type = json.type in types ? types[json.type] : [json.icon || '👍', json.type];
const [icon, theme] = json.type;
return eat(match)(
WrapPinnedBlocks(
{
type: 'rdme-callout',
data: {
hName: 'rdme-callout',
hProperties: {
theme: theme || 'default',
icon,
title: json.title,
value: json.body,
},
},
children: [
{
type: 'paragraph',
children: [{ type: 'text', value: `${icon} ` }, ...this.tokenizeInline(json.title, eat.now())],
},
...this.tokenizeBlock(json.body, eat.now()),
],
},
json
)
);
}
case 'parameters': {
const { data } = json;
const children = Object.keys(data)
.sort()
.reduce((sum, key) => {
const val = data[key];
let [row, col] = key.split('-');
row = row === 'h' ? 0 : parseInt(row, 10) + 1;
col = parseInt(col, 10);
sum[row] = sum[row] || { type: 'tableRow', children: [] };
sum[row].children[col] = {
type: row ? 'tableCell' : 'tableHead',
children: this.tokenizeInline(val, eat.now()),
};
// convert falsey values to empty strings
sum[row].children = [...sum[row].children].map(v => v || '');
return sum;
}, []);
const table = {
type: 'table',
align: 'align' in json ? json.align : new Array(json.cols).fill('left'),
children: children.filter(v => v || false),
};
return eat(match)(WrapPinnedBlocks(table, json));
}
case 'embed': {
json.title = json.title || 'Embed';
const { title, url, html } = json;
json.provider = `@${new URL(url).hostname
.split(/(?:www)?\./)
.filter(i => i)
.join('')}`;
const data = { url, html, title, provider: json.provider };
return eat(match)(
WrapPinnedBlocks(
{
type: 'embed',
...data,
children: [
{
type: 'link',
url,
title: json.provider,
children: [{ type: 'text', value: title }],
},
],
data: {
...data,
hProperties: { ...data, href: url },
hName: 'rdme-embed',
},
},
json
)
);
}
default: {
return eat(match)(
WrapPinnedBlocks(
{
type: 'div',
children: this.tokenizeBlock(json.text || json.html, eat.now()),
data: json,
},
json
)
);
}
}
}
function parser() {
const { Parser } = this;
const tokenizers = Parser.prototype.blockTokenizers;
const methods = Parser.prototype.blockMethods;
tokenizers.magicBlocks = tokenize;
methods.splice(methods.indexOf('newline'), 0, 'magicBlocks');
}
module.exports = parser;
module.exports.sanitize = sanitizeSchema => {
// const tags = sanitizeSchema.tagNames;
const attr = sanitizeSchema.attributes;
attr.li = ['className', 'checked'];
attr.pre = ['className', 'lang', 'meta'];
attr.code = ['className', 'lang', 'meta'];
attr.table = ['align'];
return parser;
};