tuture
Version:
Write tutorials from the future, with the power of Git and community.
323 lines (322 loc) • 12.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const editure_1 = require("editure");
const command_1 = require("@oclif/command");
const reload_1 = tslib_1.__importDefault(require("./reload"));
const base_1 = tslib_1.__importDefault(require("../base"));
const utils_1 = require("../utils");
const logger_1 = tslib_1.__importDefault(require("../utils/logger"));
const git_1 = require("../utils/git");
const collection_1 = require("../utils/collection");
const assets_1 = require("../utils/assets");
const internals_1 = require("../utils/internals");
const constants_1 = require("../constants");
// Internal hints for rendering code lines.
const diffRenderHints = {
hexo: {
normal: '',
add: '[tuture-add]',
del: '[tuture-del]',
omit: '\n[tuture-omit]\n',
},
plain: {
normal: '',
add: '',
del: '',
omit: '...',
},
};
const noteLevels = {
primary: { name: '主要' },
success: { name: '成功' },
info: { name: '提示' },
warning: { name: '注意' },
danger: { name: '危险' },
};
function concatCodeStr(diffItem) {
let codeStr = '';
const DIFF_ADD = [];
const DIFF_DEL = [];
diffItem.chunks.map((chunk, chunkIndex) => {
chunk.changes.map((change, index) => {
const { content, type } = change;
if (type === 'add') {
DIFF_ADD.push(index);
}
else if (type === 'del') {
DIFF_DEL.push(index);
}
// handle render code content
let code = content;
if (content !== 'normal' && content.length === 1) {
code = content.replace(/[+-]/, ' ');
}
else if (content !== 'normal' && content.length > 1) {
code = content.slice(1);
}
if (chunkIndex === diffItem.chunks.length - 1 &&
index === chunk.changes.length - 1) {
codeStr += code;
}
else {
codeStr += `${code}\n`;
}
return change;
});
return chunk;
});
return { codeStr, DIFF_ADD, DIFF_DEL };
}
class Build extends base_1.default {
// Sanitize explanation string for hexo compatibility
sanitize(text) {
if (text && this.userConfig.hexo) {
return text.replace(/`([^`\n]+)`/g, (_, code) => code.match(/[\{\}]/)
? `\`{% raw %}${code}{% endraw %}\``
: `\`${code}\``);
}
return text || '';
}
// Template for metadata of hexo posts.
hexoFrontMatterTmpl(meta) {
const { name, description, topics, categories, created, cover } = meta;
const elements = ['---', `title: "${name.replace('"', '')}"`];
if (description) {
elements.push(`description: "${this.sanitize(description).replace('"', '')}"`);
}
if (topics) {
const tags = topics
.map((topic) => `"${this.sanitize(topic)}"`)
.join(', ');
elements.push(`tags: [${tags}]`);
}
if (categories) {
const cats = categories
.map((category) => `"${this.sanitize(category)}"`)
.join(', ');
elements.push(`categories: [${cats}]`);
}
if (created) {
elements.push(`date: ${new Date(created).toISOString()}`);
}
if (cover) {
elements.push(`photos:\n - ${cover}`);
}
elements.push('---');
return elements.join('\n');
}
// Template for single line of change.
changeTmpl(content, type, newFile = false) {
let prefix = '';
const mode = this.userConfig.hexo ? 'hexo' : 'plain';
if (mode === 'plain' && type === 'del') {
return null;
}
if (!newFile) {
prefix = diffRenderHints[mode][type];
}
return prefix + content;
}
// Template for code blocks.
diffBlockTmpl(diff, hiddenLines, link) {
const filename = path_1.default.basename(diff.to || '');
const lang = filename ? filename.split('.').slice(-1)[0] : '';
const mode = this.userConfig.hexo ? 'hexo' : 'plain';
const { codeStr, DIFF_ADD, DIFF_DEL } = concatCodeStr(diff);
const code = codeStr
.split('\n')
.map((line, index) => {
if (hiddenLines && hiddenLines.includes(index)) {
if (hiddenLines.includes(index - 1)) {
// If previous line is already hidden, don't show this line.
return null;
}
const spaces = line.length - line.trimLeft().length;
return `${' '.repeat(spaces)}// ...`;
}
else if (DIFF_ADD.includes(index)) {
return this.changeTmpl(line, 'add', diff.new);
}
else if (DIFF_DEL.includes(index)) {
return this.changeTmpl(line, 'del', diff.new);
}
else {
return line;
}
})
.filter((line) => line !== null)
.map((line) => (line && line.match(/^\s+$/) ? '' : line))
.join('\n');
const head = [lang];
if (mode === 'hexo') {
if (diff.to) {
head.push(diff.to);
if (link) {
head.push(link);
head.push('查看完整代码');
}
}
}
return `\`\`\`${head.join(' ')}\n${code}\n\`\`\``;
}
noteBlockTmpl(content, level) {
if (this.userConfig.hexo) {
const title = noteLevels[level]
? `**${noteLevels[level].name}**\n\n`
: '';
return `{% note ${level} %}\n${title}${content}\n{% endnote %}`;
}
const lines = [`**${noteLevels[level].name}**`, '', ...content.split('\n')];
return lines.map((line) => (line ? `> ${line}` : '>')).join('\n');
}
getDiffFile(rawDiffs, commit, file) {
const diffItem = rawDiffs.filter((rawDiff) => utils_1.isCommitEqual(rawDiff.commit, commit))[0];
if (!diffItem) {
logger_1.default.log('warning', `Commit ${commit} is not found.`);
return null;
}
return diffItem.diff.filter((diffFile) => diffFile.to === file)[0];
}
// Markdown template for the whole tutorial.
articleTmpl(meta, steps, rawDiffs) {
const { name, description, cover, github } = meta;
const stepConverter = (node) => {
return node.children.map((n) => editure_1.toMarkdown(n)).join('\n\n');
};
const fileConverter = (node) => {
return node.display
? node.children.map((n) => editure_1.toMarkdown(n)).join('\n\n')
: '';
};
const explainConverter = (node) => {
return this.sanitize(node.children.map((n) => editure_1.toMarkdown(n)).join('\n\n'));
};
const diffBlockConverter = (node) => {
const { commit, file, hiddenLines = [] } = node;
const diff = this.getDiffFile(rawDiffs, commit, file);
const link = github ? `${github}/blob/${commit}/${file}` : undefined;
const flatHiddenLines = hiddenLines.flatMap((range) => {
const [start, end] = range;
return [...Array(end - start + 1).keys()].map((elem) => elem + start);
});
return diff ? this.diffBlockTmpl(diff, flatHiddenLines, link) : '';
};
const noteBlockConverter = (node) => {
const { level = 'default', children } = node;
const content = children.map((n) => editure_1.toMarkdown(n)).join('\n\n');
return this.noteBlockTmpl(content, level);
};
const customBlockConverters = {
step: stepConverter,
file: fileConverter,
explain: explainConverter,
['diff-block']: diffBlockConverter,
note: noteBlockConverter,
};
const elements = steps.map((step) => editure_1.toMarkdown(step, undefined, customBlockConverters));
// Add cover to the front.
if (this.userConfig.hexo) {
elements.unshift(this.hexoFrontMatterTmpl(meta), github ? internals_1.generateUserProfile(github) : '');
}
else {
elements.unshift(cover ? `` : '', this.sanitize(`# ${name}`) || '', this.sanitize(description) || '');
}
return elements
.filter((elem) => elem)
.join('\n\n')
.trim()
.replace(/\n{3,}/g, '\n\n');
}
replaceAssetPaths(tutorial, assets) {
let updated = tutorial;
// Replace all local paths.
// If not uploaded, replace it with absolute local path.
assets.forEach(({ localPath, hostingUri }) => {
updated = updated.replace(new RegExp(localPath, 'g'), hostingUri || path_1.default.resolve(localPath));
});
return updated;
}
generateTutorials(collection, rawDiffs) {
const { name, articles, description, topics, categories, github, steps, created, cover, } = collection;
const meta = {
topics,
categories,
github,
created,
name,
description,
cover,
};
const titles = articles.map((split, index) => split.name || `${name} (${index + 1})`);
// Iterate over each split of tutorial.
const tutorials = articles.map((article) => {
const articleSteps = steps.filter((step) => step.articleId === article.id);
// Override outmost metadata with article metadata.
const articleMeta = Object.assign(Object.assign({}, meta), article);
return this.articleTmpl(articleMeta, articleSteps, rawDiffs);
});
return [tutorials, titles];
}
saveTutorials(tutorials, titles, assets) {
const { buildPath } = this.userConfig;
if (!this.userConfig.out && !fs_extra_1.default.existsSync(buildPath)) {
fs_extra_1.default.mkdirSync(buildPath);
}
tutorials.forEach((tutorial, index) => {
// Path to target tutorial.
const dest = this.userConfig.out || path_1.default.join(buildPath, `${titles[index]}.md`);
// Replace local asset paths.
if (assets) {
fs_extra_1.default.writeFileSync(dest, this.replaceAssetPaths(tutorial, assets));
}
else {
fs_extra_1.default.writeFileSync(dest, tutorial);
}
logger_1.default.log('success', `Tutorial has been written to ${chalk_1.default.bold(dest)}.`);
});
}
async run() {
const { flags } = this.parse(Build);
this.userConfig = Object.assign(this.userConfig, flags);
try {
await utils_1.checkInitStatus();
}
catch (err) {
logger_1.default.log('error', err.message);
this.exit(1);
}
// Run sync command if workspace is not prepared.
if (!fs_extra_1.default.existsSync(collection_1.collectionPath) || !fs_extra_1.default.existsSync(git_1.diffPath)) {
await reload_1.default.run([]);
}
const collection = collection_1.loadCollection();
const rawDiffs = JSON.parse(fs_extra_1.default.readFileSync(constants_1.DIFF_PATH).toString());
if (rawDiffs.length === 0) {
logger_1.default.log('warning', 'No commits yet. Target tutorial will have empty content.');
}
if (flags.hexo && !collection.github) {
logger_1.default.log('warning', 'No github field provided.');
}
// COMPAT: we have to replace image paths in earlier versions of tutorials.
const assets = assets_1.loadAssetsTable();
const [tutorials, titles] = this.generateTutorials(collection, rawDiffs);
this.saveTutorials(tutorials, titles, assets);
}
}
exports.default = Build;
Build.description = 'Build tutorial into a markdown document';
Build.flags = {
help: command_1.flags.help({ char: 'h' }),
out: command_1.flags.string({
char: 'o',
description: 'name of output directory',
}),
hexo: command_1.flags.boolean({
description: 'hexo compatibility mode',
default: false,
}),
};