UNPKG

frontmatter-markdown-loader

Version:

Webpack loader for Front Matter Markdown file to get front matter attributes, compiled markdown and React/Vue component which renders compiled markdown

184 lines (156 loc) 6.35 kB
const loaderUtils = require('loader-utils') const frontmatter = require('front-matter') const Mode = require('./mode') const markdownIt = require('markdown-it'); const stringify = (src) => JSON.stringify(src).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); function getNormalizedMarkdownCompiler (options, isReactEnabled) { if (options.markdown && options.markdownIt) { throw new Error( "Both markdown and markdownIt options were specified. This is not supported. \n" + "Please refer to the documentation for usage: \n" + "https://hmsk.github.io/frontmatter-markdown-loader/options.html#markdown-compilation" ); } // If you've specified the markdown option, hand over control if (options.markdown) { return { render: options.markdown }; } // If you've passed in a MarkdownIt instance, just use that if (options.markdownIt instanceof markdownIt || (options.markdownIt && options.markdownIt.constructor && options.markdownIt.constructor.name === 'MarkdownIt')) { return options.markdownIt; } // Configuration object? Pass it to our default compiler if (typeof options.markdownIt === 'object') { return markdownIt(options.markdownIt); } // If no configuration is passed - use a sensible default return markdownIt(isReactEnabled ? { html: true, xhtmlOut: true } : { html: true }); } module.exports = function (source) { if (this.cacheable) this.cacheable(); const options = loaderUtils.getOptions(this); const requestedMode = Array.isArray(options.mode) ? options.mode : [Mode.HTML]; const enabled = (mode) => requestedMode.includes(mode); let exported = '', prependOutput = ''; const addPrepend = (code) => { prependOutput = prependOutput.concat(`${code}\n`) }; const addProperty = (key, value) => { exported += ` ${key}: ${value}, `; }; const fm = frontmatter(source); const markdownCompiler = getNormalizedMarkdownCompiler(options, enabled(Mode.REACT)); fm.html = markdownCompiler.render(fm.body); addProperty('attributes', stringify(fm.attributes)); if (enabled(Mode.HTML)) addProperty('html', stringify(fm.html)); if (enabled(Mode.BODY)) addProperty('body', stringify(fm.body)); if (enabled(Mode.META)) { const meta = { resourcePath: this.resourcePath }; addProperty('meta', stringify(meta)); } if ((enabled(Mode.VUE_COMPONENT) || enabled(Mode.VUE_RENDER_FUNCTIONS))) { let vueCompiler, compileVueTemplate; try { vueCompiler = require('vue-template-compiler') compileVueTemplate = require('@vue/component-compiler-utils').compileTemplate } catch (err) { if (err.code === "MODULE_NOT_FOUND") { throw new Error( "Failed to import vue-template-compiler or/and @vue/component-compiler-utils: \n" + "If you intend to use 'vue-component', `vue-render-functions` mode, install both to your project: \n" + "https://hmsk.github.io/frontmatter-markdown-loader/vue.html" ); } else { throw err; } } const vueRootClass = options.vue && options.vue.root ? options.vue.root : 'frontmatter-markdown'; const template = fm .html .replace(/<(code\s[^>]+)>/g, "<$1 v-pre>") .replace(/<code>/g, "<code v-pre>"); const compileOptions = { source: `<div class="${vueRootClass}">${template}</div>`, filename: this.resourcePath, compiler: vueCompiler, compilerOptions: { outputSourceRange: true }, transformAssetUrls: (options.vue && (options.vue.transformAssetUrls === false || options.vue.transformAssetUrls)) ? options.vue.transformAssetUrls : true, isProduction: process.env.NODE_ENV === 'production' }; const compiled = compileVueTemplate(compileOptions); addPrepend(`function extractVueFunctions () {\n${compiled.code}\nreturn { render: render, staticRenderFns: staticRenderFns }\n}\nconst vueFunctions = extractVueFunctions()`); let vueOutput = ''; if (enabled(Mode.VUE_RENDER_FUNCTIONS)) { vueOutput += ` render: vueFunctions.render, staticRenderFns: vueFunctions.staticRenderFns, `; } if (enabled(Mode.VUE_COMPONENT)) { vueOutput += ` component: { data: function () { return { templateRender: null } }, render: function (createElement) { return this.templateRender ? this.templateRender() : createElement("div", "Rendering"); }, created: function () { this.templateRender = vueFunctions.render; this.$options.staticRenderFns = vueFunctions.staticRenderFns; } } `; } addProperty('vue', `{${vueOutput}}`); } if (enabled(Mode.REACT)) { let babelCore; const reactRootClass = options.react && options.react.root ? options.react.root : 'frontmatter-markdown'; try { babelCore = require('@babel/core'); require('@babel/preset-react'); } catch (err) { throw new Error( "Failed to import @babel/core or/and @babel/preset-react: \n" + "If you intend to use 'react' mode, install both to your project: \n" + "https://hmsk.github.io/frontmatter-markdown-loader/react.html" ); } addPrepend(`const React = require('react')`); const escape = (str) => str.replace(/([\\`])/g, "\\$1"); const template = fm .html .replace(/<code(\s[^>]+)>(.+?)<\/code>/sg, (match, p1, p2) => `<code${p1} dangerouslySetInnerHTML={{ __html: \`${escape(p2)}\`}} />`) .replace(/<code>(.+?)<\/code>/sg, (match, p1) => `<code dangerouslySetInnerHTML={{ __html: \`${escape(p1)}\`}} />`) .replace(/<(code|pre)([^\s>]*)\sclass=([^>]+)>/g, "<$1$2 className=$3>") const compiled = babelCore .transformSync(` const markdown = <div className="${reactRootClass}"> ${template} </div> `, { presets: ['@babel/preset-react'] }); const reactComponent = ` function (props) { Object.keys(props).forEach(function (key) { this[key] = props[key] }) ${compiled.code} return markdown } ` addProperty('react', reactComponent); } return `${prependOutput}\nmodule.exports = { ${exported} }`; }