UNPKG

nobuilder

Version:

nobuilder

253 lines (226 loc) 5.67 kB
'use strict' const fs = require('fs') const path = require('path') const marked = require('marked') const rescape = require('escape-string-regexp') const PATTERN_START_TOKEN = /^`{4}(\w+)\s*/ const template = (text, values, opts) => { opts = Object.assign({ left: '{', right: '}' }, opts) let rule = new RegExp(rescape(opts.left) + '([\\w\\s\\.\\(\\)\\|-]+)' + rescape(opts.right), 'g') return text.replace(rule, (total, name) => { if (typeof values === 'function') { return values(name, total) } else { return name in values ? values[name] : total } }) } class Renderer extends marked.Renderer { constructor () { super() // codes: [{ // type: 'js', // content: <string> // }, ...] this.codes = [] this._headHtmls = [] } getHeadHtml () { return this._headHtmls.join('\n') } /** * 渲染可执行的代码片段 * @param {String} code * @param {String} lang * @param {Boolean} escaped */ code (code, lang, escaped) { let executable = false // 判断 lang 是否以 ! 开头, 如果是就是可执行的 if (lang && lang.startsWith('!')) { lang = lang.substring(1) executable = true } let parsed = super.code(code, lang, escaped) let renderred = '' // 只有可执行的代码, 才走特殊的渲染方式, codeIndex ++ if (executable) { switch (lang) { case 'js': case 'jsx': this.codes.push({ type: 'js', content: code }) let index = this.codes.length - 1 renderred = ` <div class="mount-node" id="mount-node-${index}"></div> ` break case 'css': this.codes.push({ type: 'css', content: code }) // CSS renderred = '' break case 'html': renderred = code break case 'html:head': this._headHtmls.push(code) renderred = '' break default: renderred = parsed } } else { renderred = parsed } return renderred } } function lowerCamelCase (text) { return text.replace(/-(\w)/g, function (all, letter) { return letter.toUpperCase() }) } /** * parse markdown * * source: * ``` * # header1 * * - author: zoujie.wzj * - created-time: yyyy/mm/dd * - charset: utf8 * * description ... * --- * * code ... * ``` * * result: * ``` * meta: { * charset: 'utf8', * author: 'zoujie.wzj', * createdTime: 'yyyy/mm/dd', * title: 'header1', * description: 'description ...' * } * ``` * * @param {String} content * @return {Object} * - meta * - content */ function parseMeta (content) { let lines = content.split('\n') let meta = {} let titleIndex = -1 // parse meta for (let i = 0; i < lines.length; i++) { let line = lines[i] if (line[0] === '#') { if (line[1] !== '#' && !meta.title) { meta.title = line.substring(1).trim() titleIndex = i } } else if (line.match(/^[*-]/)) { let matches = line.match(/^[*-]\s*([\w-]+)\s*:\s*(.*)/) if (matches) { let key = lowerCamelCase(matches[1]) meta[key] = String(matches[2]).trim() } lines[i] = '' } else if (!line || line.match(/^\s+/)) { continue } else { break } } // add executable mark for (let i = 0; i < lines.length; i++) { if (lines[i].match(PATTERN_START_TOKEN)) { lines[i] = lines[i].replace(PATTERN_START_TOKEN, (total, lang) => { return '````!' + lang }) } } if (meta.layout === 'test' && titleIndex >= 0) { lines[titleIndex] = '' } return { meta, content: lines.join('\n') } } function fixDocsCode (index, code, pkgName, relativePath) { return code .replace(/\bmountNode\b/g, `document.getElementById('mount-node-${index}')`) .replace(new RegExp(`(import .*["'])${pkgName}(\\/src|\\/lib)?(\\S*["'])`, 'g'), `$1${relativePath}/src$3`) } module.exports = function (content) { const cwd = process.cwd() const pkg = require(path.join(cwd, 'package.json')) const resourcePath = this.resourcePath const relativePath = path.relative(cwd, resourcePath) const parsed = parseMeta(content) const meta = parsed.meta const renderer = new Renderer() const bodyHtml = marked(parsed.content, { renderer }) const codes = renderer.codes // codes let js = [] let css = [] let realtiveRoot = path.relative(path.dirname(resourcePath), cwd) for (let i = 0; i < codes.length; i++) { let code = codes[i] if (code.type === 'js') { js.push(fixDocsCode(i, code.content, pkg.name, realtiveRoot)) } else if (code.type === 'css') { css.push(code.content) } } // params let title = meta.title || resourcePath let layout = meta.layout || 'default' let charset = meta.charset || 'utf8' let headHtml = renderer.getHeadHtml() let tplFile = path.join(__dirname, 'layout', layout + '.html') if (!fs.existsSync(tplFile)) { throw new Error(`读取 layout 文件 ${tplFile} 失败`) } let tplContent = fs.readFileSync(tplFile, 'utf8') let html = template(tplContent, { name: pkg.name, title, charset, headHtml, bodyHtml, jsUrl: `/static/js/${relativePath}.js`, css: css.join('\n') }) let htmlPath = path.relative(cwd, resourcePath).replace(/\.md$/, '.html') this.emitFile(htmlPath, html) return ` // js import React from 'react' import ReactDOM from 'react-dom' ${js.join('\n')} // HMR code if (module.hot) { module.hot.accept(); } ` }