nobuilder-json
Version:
nobuilder
263 lines (235 loc) • 5.79 kB
JavaScript
;
const fs = require("fs");
const path = require("path");
const marked = require("marked");
const rescape = require("escape-string-regexp");
const PATTERN_START_TOKEN = /^`{3}(\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();
}
`;
};