impress.me
Version:
Create impress.js presentations from markdown documents with style
232 lines (231 loc) • 8.99 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.markdownToHtml = exports.generateState = void 0;
const loglevel_1 = require("loglevel");
const helpers_1 = require("./helpers");
const fs_1 = require("fs");
const marked = require("marked");
const highlight_js_1 = require("highlight.js");
const renderers_1 = require("./renderers");
const specialLayoutSlideClasses = ['title', 'overview', 'end'];
const appendHeadingAttributes = (text, attrs, config) => {
let match = helpers_1.attrPattern.exec(text);
if (match) {
const attrText = match[3];
while ((match = helpers_1.attrItemPattern.exec(attrText))) {
const key = match[1].trim();
const value = match[2].trim();
if (key === 'class' || key === 'id' || key === 'style') {
attrs.class += ' ' + value;
}
else {
attrs['data-' + key] = value;
}
}
}
const classes = attrs.class.split(' ');
if (specialLayoutSlideClasses.find(cls => classes.includes(cls)) === undefined &&
classes.find(cls => cls.startsWith('focus') || cls.startsWith('grid')) === undefined) {
// no layout classes have been set, yet - use the slide config
attrs.class += ` ${config.layout}`;
}
if (config.primary !== 'default' && classes.find(cls => cls.includes('primary-')) === undefined) {
attrs.class += ` primary-${config.primary}`;
}
if (config.secondary !== 'default' && classes.find(cls => cls.includes('secondary-')) === undefined) {
attrs.class += ` secondary-${config.secondary}`;
}
};
exports.generateState = (headings, positionStrategy, config) => {
const outerState = headings.reduce((state, curr) => {
const root = state.root;
const isRootNode = Object.keys(state.nodes).length === 0;
const depth = isRootNode ? 1 : (config.hasInlineConfig ? curr.depth + 1 : curr.depth);
const node = Object.assign(Object.assign({}, curr), { children: [], attrs: {
class: `step slide depth-${depth}`,
}, depth });
state.nodes[curr.text] = node;
if (isRootNode) {
node.attrs.class += ' screen title';
}
appendHeadingAttributes(curr.text, node.attrs, config.slide);
node.classes = node.attrs.class.split(' ');
specialLayoutSlideClasses.forEach(id => {
if (node.classes.includes(id) && !node.attrs.id) {
node.attrs.id = id;
}
});
if (isRootNode) {
return Object.assign(Object.assign({}, state), { root: node });
}
let parent;
switch (depth) {
case 2:
node.parent = root;
root.children.push(node);
break;
case 3:
if (root.children.length === 0) {
throw new Error('Unexpected third level heading: ' + node.text);
}
parent = root.children[root.children.length - 1];
node.parent = parent;
parent.children.push(node);
break;
default:
break;
}
return state;
}, { root: {}, nodes: {}, isOpen: false });
Object.keys(outerState.nodes).forEach(key => {
const node = outerState.nodes[key];
const pos = positionStrategy.calculate(node);
node.pos = pos;
const posKeys = ['x', 'y', 'z', 'scale', 'rotate', 'rotate-x', 'rotate-y'];
posKeys.forEach(k => {
const value = parseFloat(node.attrs['data-' + k]);
if (isNaN(value)) {
if (pos[k] !== undefined) {
node.attrs['data-' + k] = String(pos[k]);
}
}
else {
pos[k] = value;
}
});
});
return outerState;
};
function cleanEmoji(s) {
return s.replace(/([\uE000-\uF8FF]|\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDDFF])/g, '');
}
const processHeading = (state, config) => {
return (text, level, raw, slugger) => {
const h = 'h' + level;
const nodeKey = text
.replace('<a href="', '[](')
.replace('"></a>', ')');
const node = state.nodes[nodeKey];
if (node === undefined) {
loglevel_1.warn('Node not found', nodeKey, Object.keys(state.nodes));
}
if (node === undefined || level > 3) {
return '<' + h + '>' + text + '</' + h + '>';
}
let html = '';
if (state.isOpen) {
html += '</div>';
}
const match = helpers_1.attrPattern.exec(text);
if (match) {
text = match[1];
}
if (level === 1 && !config.hasInlineConfig) {
config.title = config.title || text;
}
const slug = cleanEmoji(slugger.slug(text));
if (node.attrs.id === undefined) {
node.attrs.id = slug;
}
const attrList = Object.keys(node.attrs)
.filter(key => node.attrs[key] !== undefined)
.map(key => `${key}="${node.attrs[key]}"`);
html += '<div ' + attrList.join(' ') + '>';
state.isOpen = true;
html += '<' + h + ' class="heading">' + text + '</' + h + '>';
return html;
};
};
const processImage = (basePath) => (href, title, text) => {
if (href === null) {
return text;
}
if (href.startsWith('https://') || href.startsWith('http://')) {
href = helpers_1.urlToDataUri(href);
}
else {
const imageSrc = [href, basePath + '/' + href, helpers_1.resolvePath(href)].find(fs_1.existsSync);
if (imageSrc !== undefined) {
href = helpers_1.fileToDataUri(imageSrc);
}
}
let out = '<img src="' + href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"';
}
out += '>';
return out;
};
const processHighlight = (code, paramString, callback) => {
const params = paramString.split(',');
const lang = params[0];
if (params.includes('render')) {
if (renderers_1.rendererMap[lang] !== undefined) {
const options = params.slice(2).reduce((opts, curr) => {
const [key, value] = curr.split('=');
return Object.assign(Object.assign({}, opts), { [key]: value });
}, {});
renderers_1.rendererMap[lang].render(code, lang, options)
.then(rendered => callback(undefined, rendered))
.catch(err => {
loglevel_1.error('Error while rendering code block', err);
callback(err);
});
return;
}
loglevel_1.warn('No renderer for language ' + lang + ' found.');
}
callback(undefined, highlight_js_1.highlightAuto(code, [lang]).value);
};
exports.markdownToHtml = (md, config) => {
return Promise.resolve(md)
.then(md => {
const tokens = marked.lexer(md);
const headings = tokens.filter(token => token.type === 'heading');
const positionStrategy = config.positionStrategyFactory.create(config);
const state = exports.generateState(headings, positionStrategy, config);
const cleanedMarkdown = tokens.filter(token => 'raw' in token).map(token => token.raw).join('');
return [cleanedMarkdown, state];
})
.then(helpers_1.logStep('Node state generated'))
.then(([md, state]) => {
const renderer = new marked.Renderer();
renderer.heading = processHeading(state, config);
renderer.image = processImage(config.basePath);
const paragraph = renderer.paragraph;
renderer.paragraph = (text) => {
if (text.startsWith('<img src="') || text.startsWith('<a href="')) {
// omit wrapping paragraph when it starts with an image or a link
return text;
}
return paragraph(text);
};
const codeFn = renderer.code;
renderer.code = (code, language, isEscaped) => {
if (language === null || language === void 0 ? void 0 : language.split(',').includes('render')) {
return code;
}
return codeFn.bind(renderer)(code, language, isEscaped);
};
return new Promise((resolve, reject) => {
marked(md, {
gfm: true,
breaks: false,
pedantic: false,
smartLists: true,
smartypants: false,
renderer,
langPrefix: 'hljs ',
highlight: processHighlight,
}, (err, content) => {
if (err) {
return reject(err);
}
if (state.isOpen) {
content += '</div>';
}
resolve(content);
});
});
});
};