UNPKG

tuture

Version:

Write tutorials from the future, with the power of Git and community.

323 lines (322 loc) 12.7 kB
"use strict"; 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 ? `![](${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, }), };