我不知道这对谁有用,但我写了一个脚本来帮助我解决在Medium上写作的问题,我想我应该分享它。它还让我更多地使用GitHub的API,这对我来说绝对有用,所以希望它对你也有用。为什么是Medium?我自己并不喜欢这个平台,但在工作中,我们的开发者博客使用它,所以我必须使用它。总的来说,它是一个不错的平台,但有一件事它做得不好,那就是代码块。
具体来说,它不支持对你的代码样本进行任何颜色编码。对于非常短的代码块来说,这还可以,但对于任何合理大小的代码片段来说,缺乏语法着色真的开始使其难以解析。
标准 "解决方案是建立一个GitHub gist,获得gist的URL,将其粘贴到Medium中,点击回车,它将用嵌入的URL替换。这很有效,但真的很烦人。在我的上一篇文章中,我有16个这样的问题,我决定我已经受够了,是时候看看自动化工具了。这就是我所做的。
第一个解决方案
我最初的尝试(从技术上讲,它工作得很好,直到它不工作,我将在后面解释为什么)使用这个过程:
- 在一个Markdown文件中找到所有的代码块
- 对于每个代码块,尝试根据"```"后面的任何字符来识别其类型。
- 对于每一个,都要得到开头和结尾的字符位置
- 对于所有的匹配,使用基于代码块类型的文件名,创建一个新的Gist。
- 对于新创建的Gist,创建一个嵌入字符串并替换Markdown中的文本
我最初打算使用GitHub的REST APIs,但后来发现了octokit.js,这是一个实用的库,使用起来非常方便。
另外,请注意,我构建的代码需要一个个人访问令牌。你可以通过GitHub的设置快速生成这些。当需要选择作用域和权限时,只需选择gist即可,因为这里演示的代码就需要这些。
好了,让我们开始吧。首先,我的脚本需要两个输入--输入标记的位置和它应该被保存的位置:
let input = process.argv[2];
let output = process.argv[3];
if(!input || !output) {
console.log(chalk.red('Usage: node gistify.js <<input file>> <<output file>>'));
process.exit(1);
}
if(!fs.existsSync(input)) {
console.log(chalk.red(`Can't find input file ${input}`));
process.exit(1);
}
// auto remove existing output
if(fs.existsSync(output)) fs.unlinkSync(output);
然后,我读入输入文件并要求提供代码块。
let md = fs.readFileSync(input,'utf8');
console.log(chalk.green(`Parsing ${input} to find code blocks.`));
let blocks = getCodeBlocks(md);
getCodeBlocks
函数寻找代码块标记(三个回文符号)。它试图找到一个语言类型,并得到每个语言类型的范围:
function getCodeBlocks(str) {
let results = [];
let blocksReg = /```(.*?)```/sg;
let match = null;
// https://stackoverflow.com/a/2295681/52160
while((match = blocksReg.exec(str)) != null) {
let result = {
str:match[0],
start: match.index,
end: match.index + match[0].length
}
// get line one to try to figure out type
let line1 = result.str.split('\n')[0];
let type = line1.replace(/[\`\r]/g,'');
if(!type) type = 'plain';
result.type = type;
results.push(result);
}
return results;
}
注意,当一个类型没有被定义时,我把它设置为 "普通"。
一旦我得到了我的结果,我就可以检查是否有任何发现:
if(blocks.length === 0) {
console.log('No code blocks were found in this Markdown file. Have a nice day.');
process.exit(1);
}
console.log(chalk.green(`We found ${blocks.length} code blocks. Beginning the Gist conversion.`));
现在我需要对它们进行处理。我将从最后一个区块到第一个区块,因为我将在一个字符串中修改文件内容,如果我从第一个区块到最后一个区块,我的范围会有偏差:
for(let i=blocks.length-1; i >= 0; i--) {
let gist = await createGist(blocks[i].str, blocks[i].type);
// we care about HTML url
let embed = toGistEmbed(gist.html_url);
md = md.substring(0, blocks[i].start) + embed + md.substring(blocks[i].end);
console.log(chalk.yellow(`Processed ${blocks.length-i} on ${blocks.length}`));
}
让我们先来看看createGist
:
async function createGist(code, type) {
/*
We switch type to a filename, will help with code rendering.
Right now, just a few and yeah, I could just use file.TYPE except
for plain. I may come back to that.
*/
let filename = 'plain.txt';
if(type === 'js') {
filename = 'script.js';
} else if(type === 'html') {
filename = 'file.html';
} else if(type === 'py') {
filename = 'file.py';
}
// remove initial and ending ```
// oops, beginning can be ```js.
code = code.replace(/```.*/gm,'').trim();
let files = {};
files[filename] = { content: code };
let body = {
description:'',
public: true,
files
}
return (await octokit.request('POST /gists', body)).data;
}
创建Gist的API需要一个文件名。我嗅到了类型,并根据类型使用通用名称。正如评论中所说的,我可以让它更灵活一点。
然后我去掉后缀,并调用octokit。注意到这部分是多么简单--一个快速的API调用。结果是一个Gist对象,其中包括一个html_url
,我在toGistEmbed
:
function toGistEmbed(url) {
return `<script src="${url}.js"></script>`;
}
为这样一个简单的操作设置一个函数可能有点傻,但我想这有什么关系。然后我把文件写出来。这里是完整的脚本,作为Gist,因为后缀最后把我的博客的处理弄得有点乱。
所以......这个工作很好,但我遇到了一个问题。如果我把结果复制并粘贴到Medium中,它就会自动转义脚本标签并把它当作代码来显示。接下来是第二个解决方案!
第二个解决方案
为了让它在Medium中正常工作,我做了一个难以置信的小改动:
function toGistEmbed(url) {
//return `<script src="${url}.js"></script>`;
return url;
}
是的,这个函数现在什么都不做,这让它变得更加愚蠢,但我最终得到的结果是文本中包含了我的Gist网址。当我把这个粘贴到Medium时,我就去找每一个,把我的光标放在最后,然后点击回车。这仍然是一项手工工作,但比手工创建Gist要容易得多,一个一个的。
像往常一样,让我知道你的想法,如果你觉得这很有用,请给我一个赞。