背景介绍
不知道大家有没有被吐槽过文档格式看着不舒服,格式乱七八糟呢,比如说给大家看一组排版的对比:
- 不好的案例:我看你这鸡蛋不错,给我来10kg.
- 好的案例:我看你这鸡蛋不错,给我来 10 kg。
标点符号使用错误,没有空格,看着让人很不舒服😭,其实从这一个案例可以看出,规范整齐的排版对文章或者文档的阅读体验有很大的提升🔥。
而且作为一个分享者,排版的整齐就更加重要了,可以显著提升读者体验,所以我也有预谋的想要提升我的文章质量,但是人工去查排版是否规范存在很多问题:
- 人力成本高
- 无法保证 100% 都核查到
于是我想通过自动化的手段去提高文档的阅读体验,想到的办法就是通过 vscode 插件🌟,于是就诞生了我的最新插件: markdown-format 「已经可以在 vscode 插件商店搜索到了~肯定还有 bug,欢迎大家帮忙测试」。
插件中的文档排版规范来自于:译文排版规则指北
方案实现
创建一个 vscode-extension 项目
首先还是熟悉的流程,用脚手架创建一个项目:官方文档
安装 vscode 插件开发脚手架:
npm install -g yo generator-code
输入yo code
初始化代码
之后我们先去配置一下 package.json 文件:
"activationEvents": [ "onCommand:markdown-format.format" ], "main": "./extension.js", "contributes": { "commands": [ { "command": "markdown-format.format", "title": "格式化markdown" } ], "menus": { "explorer/context": [ { "command": "markdown-format.format", "when": "filesExplorerFocus", "group": "navigation@1" } ] } },
主要配置了一个 format 命令,触发时机是右键点击文件。
判断文件类型是否合规
下一步要去判断文件类型是否为 markdown,不是 markdown则不进行操作。
const isMarkdown = (path) => { if (isDir(path)) { return false; } if (extname(path) != '.md') { return false; } return true; }
词法分析
下一步就是词法分析,把 markdown 文本解析为 token 序列,其中 token 序列分为以下几种类型:
- 空格
SPACE
- 换行
ENTER
- 半角符号
HALF
- 全角符号
FULL
- 中文
CHINESE
- 数字
NUMBER
- 英文
ENGLISH
如果不了解编译原理的可以去阅读我之前的文章:前端学编译原理(一):编译引论
这里其实也就是遍历字符串的操作
function lexicalAnalysis(content) { const tokenList = []; let currentStr = ''; let currentType = ''; const handleChar = (char, type) => { if (currentType == type) { currentStr += char; } else { tokenList.push({ type: currentType, content: currentStr }) currentStr = char; currentType = type; } } for (const char of content) { if (char == ' ') { handleChar(char, 'SPACE'); } else if (char == '\n') { handleChar(char, 'ENTER'); } else if (char.match(/[\x21-\x2f\x3a-\x40\x5b-\x60\x7B-\x7F]/) || char.charCodeAt() === 8212) { //半角 和破折号 —— handleChar(char, 'HALF'); } else if (char.charCodeAt() === 12290 || (char.charCodeAt() > 65280 && char.charCodeAt() < 65375)) { //全角 (处理一下。) TODO: 为什么句号的charcode这么奇怪 handleChar(char, 'FULL'); } else if (char.match(/[\u4e00-\u9fa5]/)) { //中文 handleChar(char, 'CHINESE') } else if (char.match(/[0-9]/)) { handleChar(char, 'NUMBER'); } else { handleChar(char, 'ENGLISH'); } } tokenList.push({ type: currentType, content: currentStr }) return tokenList; }
这里给大家看一下 Token 序列的格式是什么样的
markdown 内容为:
你是`asdas`寒——草.w 主要是123ni.D... 啊。。。上不去github
Token 序列为:
0: {type: '', content: ''} 1: {type: 'CHINESE', content: '你是'} 2: {type: 'HALF', content: '`'} 3: {type: 'ENGLISH', content: 'asdas'} 4: {type: 'HALF', content: '`'} 5: {type: 'CHINESE', content: '寒'} 6: {type: 'HALF', content: '——'} 7: {type: 'CHINESE', content: '草'} 8: {type: 'HALF', content: '.'} 9: {type: 'ENGLISH', content: 'w'} 10: {type: 'SPACE', content: ' '} 11: {type: 'ENTER', content: '\n'} 12: {type: 'CHINESE', content: '主要是'} 13: {type: 'NUMBER', content: '123'} 14: {type: 'ENGLISH', content: 'ni'} 15: {type: 'HALF', content: '.'} 16: {type: 'ENGLISH', content: 'D'} 17: {type: 'HALF', content: '...'} 18: {type: 'SPACE', content: ' '} 19: {type: 'ENTER', content: '\n'} 20: {type: 'CHINESE', content: '啊'} 21: {type: 'FULL', content: '。。。'} 22: {type: 'CHINESE', content: '上不去'} 23: {type: 'ENGLISH', content: 'github'}
处理 Token 序列
这里我打算直接基于这个 Token 序列进行操作之后再组合在一起,下面我按照每条规则单独来说
中英文之间需要增加空格 中文与数字之间要有空格
- 错误:我喜欢javascript的灵活性
- 正确:我喜欢 javascript 的灵活性
这里的思路为:
- 如果我现在的
token
类型是中文并且下一个token
的类型是数字或者英文,则加入空格 - 如果我现在的
token
类型是中文并且接下来的内容是markdown
语法中的代码行高亮,加粗,斜体标识且接着数字或者英文,则也加入空格 - 如果现在匹配到的是数字或者英文,且下一个
token
的类型是中文,则加入空格 - 如果现在匹配到的是
markdown
语法中的代码行高亮,加粗,斜体标识,且前一位是英文或者数字,后一位是中文,则插入空格
if(tokenList[i].type == 'CHINESE') { if(tokenList[i+1] && ['ENGLISH', 'NUMBER'].includes(tokenList[i+1].type)) { resList.push(SPACE_TOKEN); } if(tokenList[i+2] && ['`', '**', '*'].includes(tokenList[i+1].content) && ['ENGLISH', 'NUMBER'].includes(tokenList[i+2].type)){ resList.push(SPACE_TOKEN); } } // 后 if(['ENGLISH', 'NUMBER'].includes(tokenList[i].type)) { if(tokenList[i+1] && tokenList[i+1].type === 'CHINESE') { resList.push(SPACE_TOKEN); } } if(['`', '**', '*'].includes(tokenList[i].content)) { if(resList[resList.length - 2]&&['ENGLISH', 'NUMBER'].includes(resList[resList.length - 2].type) && tokenList[i+1] && tokenList[i+1].type === 'CHINESE') { resList.push(SPACE_TOKEN); } }
数字与单位之间需要增加空格
- 错误:这筐鸡蛋有 5kg。
- 正确:这筐鸡蛋有 5 kg。
这里的思路为:
这个就是让数字和英文之间加上空格,不多介绍了「但是存在问题
」
TODO:这里存在一定的误差,我在想是否要维护一个单位列表
if(tokenList[i].type === 'NUMBER' && tokenList[i+1] && tokenList[i+1].type === 'ENGLISH') { resList.push(SPACE_TOKEN); }
全角标点与其他字符之间不加空格
- 错误:我喜欢你 , 你是小仙女。
- 正确:我喜欢你,你是小仙女。
这里的思路为:
- 如果当前字符是空格,且后一个字符是全角标点时,删除此空格
- 如果当前字符是空格,且前一个字符是全角标点时,删除此空格
if(resList[resList.length - 1].type == 'SPACE'&&tokenList[i+1]&&tokenList[i+1].type === 'FULL') { resList.pop(); } // -- 避免不一致的情况 if(resList[resList.length - 1]== 'SPACE'&&resList[resList.length - 2]&&resList[resList.length - 2].type === 'FULL') { resList.pop(); }
不重复使用标点符号
- 错误:你是卧底?!!!
- 正确:你是卧底?!
这里只对 ?,!,【,】进行处理
if(tokenList[i].type === 'FULL') { resList.pop(); let end = ''; let str = ''; for(const char of tokenList[i].content) { if(end == char && ["?", "!", "【", "】"].includes(char)){ continue; } else { str += char; } } resList.push({ type: 'FULL', content: str }); }
省略号
- 错误:这里有西红柿,土豆,芹菜。。。
- 正确:这里有西红柿,土豆,芹菜……
省略号应该显示为:……
if(resList[resList.length - 2] && ['CHINESE', 'ENGLISH'].includes(resList[resList.length - 2].type) && tokenList[i].content.match(/^[。\.]{2,}$/)){ resList.pop(); resList.push({ type: 'HALF', content: '……' }) }
中文接的是全角字符
- 错误:你好,我叫寒草.
- 正确:你好,我叫寒草。
暂时支持切换的标点:
- 。.
- ,,
- ??
- !!
- ;;
- ::
考虑到闭合标签存在一些特殊情况,暂未做处理
if(['.', '?', '!', ';', ':'].includes(tokenList[i].content)) { if(resList[resList.length - 2] && resList[resList.length - 2].type == 'CHINESE') { resList.pop(); resList.push({ type: 'FULL', content: punctuationMap.get(tokenList[i].content) }); } }
英文接的是半角字符
- 错误:I am hancao。
- 正确:I am hancao.
英文与中文同理~
if(['。', '?', '!', ';', ':'].includes(tokenList[i].content)) { if(resList[resList.length - 2] && resList[resList.length - 2].type == 'ENGLISH') { resList.pop(); resList.push({ type: 'HALF', content: punctuationMap.get(tokenList[i].content) }); } }
功能演示
格式化前:
格式化后:
结束语
目前肯定有一定的 bug,我也会在自己的使用中不断完善
如果大家喜欢我的文章,点赞与关注就是对我最大的支持🌿,感谢~
写在最后
你让我的世界五彩斑斓
class World { constructor(name) { this.isColorful = false; this.owner = name; } add(somebody) { if(somebody === 'YOU' && this.owner === '寒草') { this.isColorful = true; } } }