[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"content-scripts-to-improve-md-writing":3,"$ffmv0os7g0tbd":21},{"id":4,"type":5,"slug":6,"title":7,"date":8,"category":9,"tags":10,"body_markdown":13,"permalink":14,"excerpt_src":14,"media_type":14,"media_title":14,"media_author":14,"media_url":14,"rating":14,"layout":14,"pv":15,"admin_only":15,"created_at":16,"updated_at":16,"deleted_at":14,"status":17,"html":18,"excerpt":19,"cover":20},130,"article","scripts-to-improve-md-writing","使用脚本改进VSCode Markdown博客编写体验","2022-08-25 08:46","tech",[11,12],"Markdown","图片","\n## 背景\n\n本博客用的是Hexo，最近迁移了很多以前写在别处的文章过来，在迁移过程中发现最不方便的一件事情是图片的处理。\n\n大概的目录结构：\n\n```\n- source\n    - _posts  文章目录\n        - hello1.md  文章\n        - hello2.md  文章\n    - assets  图片目录\n        - hello1  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n        - hello2  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n```\n\n因为使用的是通用的工具（如VSCode）来编写Markdown，没有专门针对图片资源做额外的处理，因此在写文章的时候如果碰到需要插图，需要经历以下几步：\n\n1. 打开图片目录\n2. 新建与文章对应的子目录\n3. 放入图片\n4. 重命名图片\n5. 在文章中插入图片标记，类似这样`![image 01](\u002Fassets\u002Fhello1\u002F01.jpg)`\n\n其中第1步和第5步都涉及到路径的问题，而且都需要手工进入或者手工输入，当路径比较深或者文章名称比较长的时候，既不方便也容易出错。\n\n\u003C!-- more -->\n\n## 目标\n\n在忍受了这种不方便很久之后，本着“重复的事情一定可以用代码解决”的想法，我决定找到一种更自动化的方式来做。首先，定义一下目标：\n\n1. 图片存入与文章相关的路径\n\n    因为我的图片路径是与文章相关的，因此固定将图片统一放到某个目录的方式不纳入考虑，这就排除了很多现成的解决方案。\n\n    希望这个方案可以自动建立与文章相关联的目录并打开，以便放入图片。\n2. 自动完成重命名\n3. 自动将图片地址填入文章中预留好的位置\n\n## 实现\n\n首先，新建一个脚本放入博客目录中，以便调用：`utils\u002Fassets.js`。\n\n### VSCode调用\n\n当需要插图的时候，对应的文章刚好是VSCode中当前编辑文件，因此希望能将当前文件路径直接传递给脚本。这里使用了一个插件：[Command Runner](https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=edonet.vscode-command-runner)。\n\n这个插件可以通过VSCode配置或者`package.json`来定义一些自定义的命令。于是我在`package.json`中加了这么一段：\n\n```json\n{\n    \"commands\": {\n        \"assets\": \".\u002Futils\u002Fassets.js ${file}\"\n    }\n}\n```\n\n这样就可以通过VSCode直接调用脚本并且传入当前文章的信息。\n\n![command runner 1](\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F01.png)\n\n![command runner 2](\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F02.png)\n\n接下来就是脚本的实现了。\n\n### 新建图片目录\n\n原理比较简单，首先计算一下文章对应的图片目录，然后使用`fs.mkdirSync`新建对应的目录。最后为了方便使用，调用一下系统的`open`命令，把刚新建好的目录打开。\n\n```javascript\nconst fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst mdPath = process.argv[2];\n\nif(!mdPath) {\n    console.log('md file path required.');\n    process.exit(1);\n}\n\nconst assetsPath = mdPath.replace(\u002F\\\u002F_posts\\\u002F\u002F, '\u002Fassets\u002F').replace(\u002F.md$\u002F, '');\nfs.mkdirSync(assetsPath, { recursive: true });\n\nconsole.log('opening ' + assetsPath);\nspawn('open', [assetsPath]);\n```\n\n> 注意：这个代码实现比较粗糙：\n>\n> 1. 在计算图片目录时粗暴地使用了`replace`来替换，更稳妥的办法是使用相对路径来计算\n> 2. 目录路径未考虑windows系统，这里应该使用`path.sep`更好\n> 3. 未考虑新建目录失败的情况\n> 4. 未考虑没有`open`命令的系统，应该使用封装好的库更好\n\n### 自动重命名\n\n要想让代码为我们放进去的文件自动重命名，首先需要让代码知道“有一个文件被放进去了”，这里就需要使用到`fs.watch`。这个方法可以监听一个目录，当目录发生变化时触发回调，这样我们就可以在回调中完成重命名。\n\n> 注意：`fs.watch`并不是非常好用，在macOS下放入一个文件，会触发`rename`事件两次，不管是事件名称还是触发次数都不太正常。更好的方法是使用`chokidar`这样的库来监听变化。\n\n我这里选择使用数字编号来命名，如果目录中已经有文件存在的话，就需要先知道最新的编号是多少。\n\n```javascript\nlet index = 0;\nconst files = fs.readdirSync(assetsPath);\nfiles.forEach((filename) => {\n    if (\u002F^\\.\u002F.test(filename)) return;\n    if (!\u002F^[\\d]+\\..*$\u002F.test(filename)) return;\n    index = Number(filename.replace(\u002F[^\\d]\u002Fg, ''));\n});\nconsole.log('lastIndex: ' + index);\n```\n\n这段代码首先将序号`index`设为`0`，然后对以纯数字命名的文件进行扫描并解析其数字作为最新的序号。\n\n> 注：这个逻辑也不严谨，如果数字位数不同的话，顺序不是按序号排列的，可能得到错误的结果。但因为我的命令规则全部是2位数字，所以默认这个问题不存在，忽略了。\n\n接下来是监视新的文件并重命名：\n\n```javascript\nconst getNewName = (filename, isPrev = false) => {\n    if (!filename) return filename;\n    const targetIndex = isPrev ? index : index + 1;\n    return filename.replace(\u002F.*(\\..*)$\u002F, (targetIndex + '').padStart(2, '0') + '$1');\n}\n\nfs.watch(assetsPath, {\n    persistent: true,\n}, (e, filename) => {\n    if (\u002F^\\.\u002F.test(filename)) return;\n    if (filename === getNewName(filename, true)) return;\n    setTimeout(() => {\n        try{\n            const newName = getNewName(filename);\n            const oldPath = path.join(assetsPath, filename);\n            const newPath = path.join(assetsPath, newName);\n            fs.renameSync(oldPath, newPath);\n            console.log(filename + ' renamed to ' + newName);\n            index++;\n            \u002F\u002F fillMd(newPath);\n        } catch (e) {\n            \u002F\u002F nothing\n        }\n    }, 500);\n});\n```\n\n这段代码首先定义了一个`getNewName()`方法，用于获取即将被重命名的文件的新文件名。这个方法接受一个`isPrev`参数，它的作用是判断要获取“前一个（已生成）”的文件名，还是“下一个（即将生成）”的文件名。\n\n因为`fs.watch`对新文件和重命名的文件都触发`rename`事件，因此无法区分是被放入了一个新文件，还是之前放入的文件被代码重命名了。这里要做一个判断，将文件名与“前一个（已生成）”的文件名对比，如果相同，说明是刚刚重命名过的文件。\n\n接下来的逻辑比较好理解，就是重命名的过程了，如果成功，就将序号`+1`。\n\n> 值得注意的2个点：\n>\n> 1. `setTimeout`的作用，主要是因为事件会连续触发两次，希望在事件都触发完之后再处理（可能并不是必要的）\n> 2. `try...catch`的使用，也是因为事件会连续触发两次，第2次一定会失败，因此加一个`try...catch`，并不是为了考虑严谨\n\n## 填入Markdown指定位置\n\n在上面的代码中有一个注释的`fillMd()`的调用，它的作用就是将图片地址填入Markdown指定位置。\n\n具体而言，在编写文章时，我会先留一个“洞”，也就是图片标记中的地址是空的：\n\n```\n![图片描述]()\n```\n\n在调用`fillMd()`时会将图片地址填入上述标记中的“洞”：\n\n```javascript\nconst fillMd = (imagePath) => {\n    const mdContent = fs.readFileSync(mdPath, 'utf-8');\n    const imageRelativePath = imagePath.replace(\u002F^.*\\\u002Fsource\\\u002Fassets\\\u002F\u002F, '\u002Fassets\u002F');\n    const newContent = mdContent.replace(\u002F\\!\\[(.*?)\\]\\(\\)\u002F, '![$1](' + imageRelativePath + ')');\n    fs.writeFileSync(mdPath, newContent);\n};\n```\n\n原理也很简单，计算图片的地址并读取Markdown文件，然后通过正则表达式替换的方式将地址填进去，再将新内容写入原文件即可。注意这里的正则表达式没有加`\u002Fg`标记，也就是只替换找到的第一处“洞”，说人话就是一次只填一个，这正是预期的工作方式。\n\n## 效果\n\n有了脚本之后改进的流程：\n\n1. 写文章&留好“洞”（可以写一个就往下处理一个图片，也可以全部写好，最后一起处理）\n2. 运行“assets”脚本\n3. 在自动打开的文件夹中放入图片\n4. 没有然后了\n\n演示视频\u003Chttps:\u002F\u002Ftwitter.com\u002FTooooooBug\u002Fstatus\u002F1562273683246555142>（需要翻墙）。\n\n## 总结和展望\n\n我始终认为，写代码是为了解决问题，至于写得是否完备是否漂亮都是其次的。这个例子其实也是一次“为自己写代码来解决问题”的过程，整个代码只有60行，尽管它不是很严谨，也不是很完美，但仍然算是很满意的一次实践。\n\n如果这个工具继续做下去的话，可能会有几个方向：\n\n1. 做成npm包，提升易用性\n2. 适配更多的文章\u002F图片目录规则，以适应更多的情况\n3. 替换一些工具\u002F写法，提升代码严谨性\n4. 加入更多的功能，比如\n    1. 反过来从md文本中获取图片信息，然后移动\u002F下载保存等\n    2. 加入图床自动上传和替换功能\n    3. ...\n\n不过我只是想把它当成解决自己问题的小工具，因此可能不会再继续花时间深入了，有兴趣的朋友可以参照一下做出更适合自己的工具。\n\n## 附：对Twitter上一些观念的回复\n\n1. 可以用XXX软件：是可以的，但之所以选择Markdown来写东西就是看中了纯文本的通用性，不希望被绑在某个工具上。事实上我也从来不用任何Markdown工具的高级功能，我觉得对纯文本每个字节的掌控是非常必要的。这个脚本也是通用的，你可以手工运行它。\n2. 可以用笔记软件：我认为笔记和文章\u002F博客写作是两种不同的用途。尽管有人喜欢all in one，把文章\u002F资料采集\u002F知识管理\u002F分享\u002Ftodo\u002FOKR等都放入笔记软件，但我不喜欢，我认为笔记就是用来记录的，我自己写的笔记软件也没有任何采集\u002F分享之类的功能，仅仅是输入、记录和搜索而已。\n3. 可以用图床类工具：个人不喜欢将图片和文章分开保存，这些松散的结构间很难建立起联系，在整理的时候会非常困难。此外图床类也会涉及到付费\u002FCDN\u002F防盗链等额外工作。\n\n当然，尽管现阶段不适用于我的文章\u002F博客的场景，但推友们的回复仍然有不少值得学习的地方，也有不少好工具，值得收藏。\n",null,0,"2026-08-28 04:37:17","published","\u003Ch2>背景\u003C\u002Fh2>\n\u003Cp>本博客用的是Hexo，最近迁移了很多以前写在别处的文章过来，在迁移过程中发现最不方便的一件事情是图片的处理。\u003C\u002Fp>\n\u003Cp>大概的目录结构：\u003C\u002Fp>\n\u003Cpre class=\"shiki\" style=\"background-color:#121212;color:#dbd7caee\" tabindex=\"0\">\u003Ccode>- source\n    - _posts  文章目录\n        - hello1.md  文章\n        - hello2.md  文章\n    - assets  图片目录\n        - hello1  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n        - hello2  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>因为使用的是通用的工具（如VSCode）来编写Markdown，没有专门针对图片资源做额外的处理，因此在写文章的时候如果碰到需要插图，需要经历以下几步：\u003C\u002Fp>\n\u003Col>\n\u003Cli>打开图片目录\u003C\u002Fli>\n\u003Cli>新建与文章对应的子目录\u003C\u002Fli>\n\u003Cli>放入图片\u003C\u002Fli>\n\u003Cli>重命名图片\u003C\u002Fli>\n\u003Cli>在文章中插入图片标记，类似这样\u003Ccode>![image 01](\u002Fassets\u002Fhello1\u002F01.jpg)\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>其中第1步和第5步都涉及到路径的问题，而且都需要手工进入或者手工输入，当路径比较深或者文章名称比较长的时候，既不方便也容易出错。\u003C\u002Fp>\n\u003Ch2>目标\u003C\u002Fh2>\n\u003Cp>在忍受了这种不方便很久之后，本着“重复的事情一定可以用代码解决”的想法，我决定找到一种更自动化的方式来做。首先，定义一下目标：\u003C\u002Fp>\n\u003Col>\n\u003Cli>\n\u003Cp>图片存入与文章相关的路径\u003C\u002Fp>\n\u003Cp>因为我的图片路径是与文章相关的，因此固定将图片统一放到某个目录的方式不纳入考虑，这就排除了很多现成的解决方案。\u003C\u002Fp>\n\u003Cp>希望这个方案可以自动建立与文章相关联的目录并打开，以便放入图片。\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>自动完成重命名\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>自动将图片地址填入文章中预留好的位置\u003C\u002Fp>\n\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>实现\u003C\u002Fh2>\n\u003Cp>首先，新建一个脚本放入博客目录中，以便调用：\u003Ccode>utils\u002Fassets.js\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Ch3>VSCode调用\u003C\u002Fh3>\n\u003Cp>当需要插图的时候，对应的文章刚好是VSCode中当前编辑文件，因此希望能将当前文件路径直接传递给脚本。这里使用了一个插件：\u003Ca href=\"https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=edonet.vscode-command-runner\">Command Runner\u003C\u002Fa>。\u003C\u002Fp>\n\u003Cp>这个插件可以通过VSCode配置或者\u003Ccode>package.json\u003C\u002Fcode>来定义一些自定义的命令。于是我在\u003Ccode>package.json\u003C\u002Fcode>中加了这么一段：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n    &quot;commands&quot;: {\n        &quot;assets&quot;: &quot;.\u002Futils\u002Fassets.js ${file}&quot;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这样就可以通过VSCode直接调用脚本并且传入当前文章的信息。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F01.png\" alt=\"command runner 1\">\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F02.png\" alt=\"command runner 2\">\u003C\u002Fp>\n\u003Cp>接下来就是脚本的实现了。\u003C\u002Fp>\n\u003Ch3>新建图片目录\u003C\u002Fh3>\n\u003Cp>原理比较简单，首先计算一下文章对应的图片目录，然后使用\u003Ccode>fs.mkdirSync\u003C\u002Fcode>新建对应的目录。最后为了方便使用，调用一下系统的\u003Ccode>open\u003C\u002Fcode>命令，把刚新建好的目录打开。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const fs = require('fs');\nconst path = require('path');\nconst { spawn } = require('child_process');\n\nconst mdPath = process.argv[2];\n\nif(!mdPath) {\n    console.log('md file path required.');\n    process.exit(1);\n}\n\nconst assetsPath = mdPath.replace(\u002F\\\u002F_posts\\\u002F\u002F, '\u002Fassets\u002F').replace(\u002F.md$\u002F, '');\nfs.mkdirSync(assetsPath, { recursive: true });\n\nconsole.log('opening ' + assetsPath);\nspawn('open', [assetsPath]);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>注意：这个代码实现比较粗糙：\u003C\u002Fp>\n\u003Col>\n\u003Cli>在计算图片目录时粗暴地使用了\u003Ccode>replace\u003C\u002Fcode>来替换，更稳妥的办法是使用相对路径来计算\u003C\u002Fli>\n\u003Cli>目录路径未考虑windows系统，这里应该使用\u003Ccode>path.sep\u003C\u002Fcode>更好\u003C\u002Fli>\n\u003Cli>未考虑新建目录失败的情况\u003C\u002Fli>\n\u003Cli>未考虑没有\u003Ccode>open\u003C\u002Fcode>命令的系统，应该使用封装好的库更好\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fblockquote>\n\u003Ch3>自动重命名\u003C\u002Fh3>\n\u003Cp>要想让代码为我们放进去的文件自动重命名，首先需要让代码知道“有一个文件被放进去了”，这里就需要使用到\u003Ccode>fs.watch\u003C\u002Fcode>。这个方法可以监听一个目录，当目录发生变化时触发回调，这样我们就可以在回调中完成重命名。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>注意：\u003Ccode>fs.watch\u003C\u002Fcode>并不是非常好用，在macOS下放入一个文件，会触发\u003Ccode>rename\u003C\u002Fcode>事件两次，不管是事件名称还是触发次数都不太正常。更好的方法是使用\u003Ccode>chokidar\u003C\u002Fcode>这样的库来监听变化。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>我这里选择使用数字编号来命名，如果目录中已经有文件存在的话，就需要先知道最新的编号是多少。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>let index = 0;\nconst files = fs.readdirSync(assetsPath);\nfiles.forEach((filename) =&gt; {\n    if (\u002F^\\.\u002F.test(filename)) return;\n    if (!\u002F^[\\d]+\\..*$\u002F.test(filename)) return;\n    index = Number(filename.replace(\u002F[^\\d]\u002Fg, ''));\n});\nconsole.log('lastIndex: ' + index);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这段代码首先将序号\u003Ccode>index\u003C\u002Fcode>设为\u003Ccode>0\u003C\u002Fcode>，然后对以纯数字命名的文件进行扫描并解析其数字作为最新的序号。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>注：这个逻辑也不严谨，如果数字位数不同的话，顺序不是按序号排列的，可能得到错误的结果。但因为我的命令规则全部是2位数字，所以默认这个问题不存在，忽略了。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>接下来是监视新的文件并重命名：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const getNewName = (filename, isPrev = false) =&gt; {\n    if (!filename) return filename;\n    const targetIndex = isPrev ? index : index + 1;\n    return filename.replace(\u002F.*(\\..*)$\u002F, (targetIndex + '').padStart(2, '0') + '$1');\n}\n\nfs.watch(assetsPath, {\n    persistent: true,\n}, (e, filename) =&gt; {\n    if (\u002F^\\.\u002F.test(filename)) return;\n    if (filename === getNewName(filename, true)) return;\n    setTimeout(() =&gt; {\n        try{\n            const newName = getNewName(filename);\n            const oldPath = path.join(assetsPath, filename);\n            const newPath = path.join(assetsPath, newName);\n            fs.renameSync(oldPath, newPath);\n            console.log(filename + ' renamed to ' + newName);\n            index++;\n            \u002F\u002F fillMd(newPath);\n        } catch (e) {\n            \u002F\u002F nothing\n        }\n    }, 500);\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这段代码首先定义了一个\u003Ccode>getNewName()\u003C\u002Fcode>方法，用于获取即将被重命名的文件的新文件名。这个方法接受一个\u003Ccode>isPrev\u003C\u002Fcode>参数，它的作用是判断要获取“前一个（已生成）”的文件名，还是“下一个（即将生成）”的文件名。\u003C\u002Fp>\n\u003Cp>因为\u003Ccode>fs.watch\u003C\u002Fcode>对新文件和重命名的文件都触发\u003Ccode>rename\u003C\u002Fcode>事件，因此无法区分是被放入了一个新文件，还是之前放入的文件被代码重命名了。这里要做一个判断，将文件名与“前一个（已生成）”的文件名对比，如果相同，说明是刚刚重命名过的文件。\u003C\u002Fp>\n\u003Cp>接下来的逻辑比较好理解，就是重命名的过程了，如果成功，就将序号\u003Ccode>+1\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>值得注意的2个点：\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Ccode>setTimeout\u003C\u002Fcode>的作用，主要是因为事件会连续触发两次，希望在事件都触发完之后再处理（可能并不是必要的）\u003C\u002Fli>\n\u003Cli>\u003Ccode>try...catch\u003C\u002Fcode>的使用，也是因为事件会连续触发两次，第2次一定会失败，因此加一个\u003Ccode>try...catch\u003C\u002Fcode>，并不是为了考虑严谨\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fblockquote>\n\u003Ch2>填入Markdown指定位置\u003C\u002Fh2>\n\u003Cp>在上面的代码中有一个注释的\u003Ccode>fillMd()\u003C\u002Fcode>的调用，它的作用就是将图片地址填入Markdown指定位置。\u003C\u002Fp>\n\u003Cp>具体而言，在编写文章时，我会先留一个“洞”，也就是图片标记中的地址是空的：\u003C\u002Fp>\n\u003Cpre class=\"shiki\" style=\"background-color:#121212;color:#dbd7caee\" tabindex=\"0\">\u003Ccode>![图片描述]()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在调用\u003Ccode>fillMd()\u003C\u002Fcode>时会将图片地址填入上述标记中的“洞”：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const fillMd = (imagePath) =&gt; {\n    const mdContent = fs.readFileSync(mdPath, 'utf-8');\n    const imageRelativePath = imagePath.replace(\u002F^.*\\\u002Fsource\\\u002Fassets\\\u002F\u002F, '\u002Fassets\u002F');\n    const newContent = mdContent.replace(\u002F\\!\\[(.*?)\\]\\(\\)\u002F, '![$1](' + imageRelativePath + ')');\n    fs.writeFileSync(mdPath, newContent);\n};\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>原理也很简单，计算图片的地址并读取Markdown文件，然后通过正则表达式替换的方式将地址填进去，再将新内容写入原文件即可。注意这里的正则表达式没有加\u003Ccode>\u002Fg\u003C\u002Fcode>标记，也就是只替换找到的第一处“洞”，说人话就是一次只填一个，这正是预期的工作方式。\u003C\u002Fp>\n\u003Ch2>效果\u003C\u002Fh2>\n\u003Cp>有了脚本之后改进的流程：\u003C\u002Fp>\n\u003Col>\n\u003Cli>写文章&amp;留好“洞”（可以写一个就往下处理一个图片，也可以全部写好，最后一起处理）\u003C\u002Fli>\n\u003Cli>运行“assets”脚本\u003C\u002Fli>\n\u003Cli>在自动打开的文件夹中放入图片\u003C\u002Fli>\n\u003Cli>没有然后了\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>演示视频\u003Ca href=\"https:\u002F\u002Ftwitter.com\u002FTooooooBug\u002Fstatus\u002F1562273683246555142\">https:\u002F\u002Ftwitter.com\u002FTooooooBug\u002Fstatus\u002F1562273683246555142\u003C\u002Fa>（需要翻墙）。\u003C\u002Fp>\n\u003Ch2>总结和展望\u003C\u002Fh2>\n\u003Cp>我始终认为，写代码是为了解决问题，至于写得是否完备是否漂亮都是其次的。这个例子其实也是一次“为自己写代码来解决问题”的过程，整个代码只有60行，尽管它不是很严谨，也不是很完美，但仍然算是很满意的一次实践。\u003C\u002Fp>\n\u003Cp>如果这个工具继续做下去的话，可能会有几个方向：\u003C\u002Fp>\n\u003Col>\n\u003Cli>做成npm包，提升易用性\u003C\u002Fli>\n\u003Cli>适配更多的文章\u002F图片目录规则，以适应更多的情况\u003C\u002Fli>\n\u003Cli>替换一些工具\u002F写法，提升代码严谨性\u003C\u002Fli>\n\u003Cli>加入更多的功能，比如\n\u003Col>\n\u003Cli>反过来从md文本中获取图片信息，然后移动\u002F下载保存等\u003C\u002Fli>\n\u003Cli>加入图床自动上传和替换功能\u003C\u002Fli>\n\u003Cli>...\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>不过我只是想把它当成解决自己问题的小工具，因此可能不会再继续花时间深入了，有兴趣的朋友可以参照一下做出更适合自己的工具。\u003C\u002Fp>\n\u003Ch2>附：对Twitter上一些观念的回复\u003C\u002Fh2>\n\u003Col>\n\u003Cli>可以用XXX软件：是可以的，但之所以选择Markdown来写东西就是看中了纯文本的通用性，不希望被绑在某个工具上。事实上我也从来不用任何Markdown工具的高级功能，我觉得对纯文本每个字节的掌控是非常必要的。这个脚本也是通用的，你可以手工运行它。\u003C\u002Fli>\n\u003Cli>可以用笔记软件：我认为笔记和文章\u002F博客写作是两种不同的用途。尽管有人喜欢all in one，把文章\u002F资料采集\u002F知识管理\u002F分享\u002Ftodo\u002FOKR等都放入笔记软件，但我不喜欢，我认为笔记就是用来记录的，我自己写的笔记软件也没有任何采集\u002F分享之类的功能，仅仅是输入、记录和搜索而已。\u003C\u002Fli>\n\u003Cli>可以用图床类工具：个人不喜欢将图片和文章分开保存，这些松散的结构间很难建立起联系，在整理的时候会非常困难。此外图床类也会涉及到付费\u002FCDN\u002F防盗链等额外工作。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>当然，尽管现阶段不适用于我的文章\u002F博客的场景，但推友们的回复仍然有不少值得学习的地方，也有不少好工具，值得收藏。\u003C\u002Fp>\n","\u003Ch2>背景\u003C\u002Fh2>\n\u003Cp>本博客用的是Hexo，最近迁移了很多以前写在别处的文章过来，在迁移过程中发现最不方便的一件事情是图片的处理。\u003C\u002Fp>\n\u003Cp>大概的目录结构：\u003C\u002Fp>\n\u003Cpre class=\"shiki\" style=\"background-color:#121212;color:#dbd7caee\" tabindex=\"0\">\u003Ccode>- source\n    - _posts  文章目录\n        - hello1.md  文章\n        - hello2.md  文章\n    - assets  图片目录\n        - hello1  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n        - hello2  与文章对应的子目录\n            - 01.jpg\n            - 02.jpg\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>因为使用的是通用的工具（如VSCode）来编写Markdown，没有专门针对图片资源做额外的处理，因此在写文章的时候如果碰到需要插图，需要经历以下几步：\u003C\u002Fp>\n\u003Col>\n\u003Cli>打开图片目录\u003C\u002Fli>\n\u003Cli>新建与文章对应的子目录\u003C\u002Fli>\n\u003Cli>放入图片\u003C\u002Fli>\n\u003Cli>重命名图片\u003C\u002Fli>\n\u003Cli>在文章中插入图片标记，类似这样\u003Ccode>![image 01](\u002Fassets\u002Fhello1\u002F01.jpg)\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>其中第1步和第5步都涉及到路径的问题，而且都需要手工进入或者手工输入，当路径比较深或者文章名称比较长的时候，既不方便也容易出错。\u003C\u002Fp>\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2022\u002Fscripts-to-improve-md-writing\u002F01.png",{"total":15,"totalRoots":15,"comments":22,"pv":15},[]]