nobuilder
Version:
nobuilder
253 lines (226 loc) • 5.67 kB
JavaScript
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();
}
`
}