UNPKG

impress.me

Version:

Create impress.js presentations from markdown documents with style

232 lines (231 loc) 8.99 kB
"use strict"; 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); }); }); }); };