如何将Markdown代码块转换为Gists(附代码示例)

165 阅读4分钟

我不知道这对谁有用,但我写了一个脚本来帮助我解决在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要容易得多,一个一个的。

像往常一样,让我知道你的想法,如果你觉得这很有用,请给我一个赞。